diff --git a/.github/workflows/osv-scanner-pr.yml b/.github/workflows/osv-scanner-pr.yml deleted file mode 100644 index 02cc7cbb8c9..00000000000 --- a/.github/workflows/osv-scanner-pr.yml +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: OSV-Scanner PR Scan - -on: - pull_request: - branches: [main] - merge_group: - branches: [main] - -jobs: - scan-pr: - uses: "./.github/workflows/osv-scanner-reusable-pr.yml" - with: - # Just scan the root directory and docs, since everything else is fixtures - scan-args: |- - --skip-git - ./ - ./docs/ - permissions: - security-events: write - contents: read diff --git a/.github/workflows/osv-scanner-scheduled.yml b/.github/workflows/osv-scanner-unified-action.yml similarity index 59% rename from .github/workflows/osv-scanner-scheduled.yml rename to .github/workflows/osv-scanner-unified-action.yml index d863a71ee44..e9489442eee 100644 --- a/.github/workflows/osv-scanner-scheduled.yml +++ b/.github/workflows/osv-scanner-unified-action.yml @@ -15,13 +15,24 @@ name: OSV-Scanner Scheduled Scan on: + pull_request: + branches: ["main"] + merge_group: + branches: ["main"] schedule: - cron: "12 12 * * 1" push: branches: ["main"] +permissions: + # Require writing security events to upload SARIF file to security tab + security-events: write + # Read commit contents + contents: read + jobs: scan-scheduled: + if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }} uses: "./.github/workflows/osv-scanner-reusable.yml" with: # Just scan the root directory and docs, since everything else is fixtures @@ -29,8 +40,12 @@ jobs: --skip-git ./ ./docs/ - permissions: - # Require writing security events to upload SARIF file to security tab - security-events: write - # Read commit contents - contents: read + scan-pr: + if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} + uses: "./.github/workflows/osv-scanner-reusable-pr.yml" + with: + # Just scan the root directory and docs, since everything else is fixtures + scan-args: |- + --skip-git + ./ + ./docs/ diff --git a/.golangci.yaml b/.golangci.yaml index 53a86bbeefd..e8ba6b97bda 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -42,6 +42,12 @@ linters: - unused linters-settings: + govet: + settings: + printf: + funcs: + - (github.com/google/osv-scanner/pkg/reporter.Reporter).PrintErrorf + - (github.com/google/osv-scanner/pkg/reporter.Reporter).PrintTextf depguard: rules: regexp: @@ -66,6 +72,9 @@ linters-settings: issues: exclude-rules: + - path: pkg/reporter + linters: + - dupl - path: _test\.go linters: - goerr113 diff --git a/cmd/osv-reporter/main.go b/cmd/osv-reporter/main.go index 5c7e8b4354d..f4f3d13706d 100644 --- a/cmd/osv-reporter/main.go +++ b/cmd/osv-reporter/main.go @@ -42,7 +42,7 @@ func run(args []string, stdout, stderr io.Writer) int { cli.VersionPrinter = func(ctx *cli.Context) { // Use the app Writer and ErrWriter since they will be the writers to keep parallel tests consistent tableReporter = reporter.NewTableReporter(ctx.App.Writer, ctx.App.ErrWriter, false, 0) - tableReporter.PrintText(fmt.Sprintf("osv-scanner version: %s\ncommit: %s\nbuilt at: %s\n", ctx.App.Version, commit, date)) + tableReporter.PrintTextf("osv-scanner version: %s\ncommit: %s\nbuilt at: %s\n", ctx.App.Version, commit, date) } app := &cli.App{ @@ -179,11 +179,11 @@ func run(args []string, stdout, stderr io.Writer) int { } if errors.Is(err, osvscanner.NoPackagesFoundErr) { - tableReporter.PrintError("No package sources found, --help for usage information.\n") + tableReporter.PrintErrorf("No package sources found, --help for usage information.\n") return 128 } - tableReporter.PrintError(fmt.Sprintf("%v\n", err)) + tableReporter.PrintErrorf("%v\n", err) } // if we've been told to print an error, and not already exited with diff --git a/cmd/osv-scanner/main.go b/cmd/osv-scanner/main.go index 8952b8005e3..ebfd2e3c867 100644 --- a/cmd/osv-scanner/main.go +++ b/cmd/osv-scanner/main.go @@ -28,7 +28,7 @@ func run(args []string, stdout, stderr io.Writer) int { cli.VersionPrinter = func(ctx *cli.Context) { // Use the app Writer and ErrWriter since they will be the writers to keep parallel tests consistent r = reporter.NewTableReporter(ctx.App.Writer, ctx.App.ErrWriter, false, 0) - r.PrintText(fmt.Sprintf("osv-scanner version: %s\ncommit: %s\nbuilt at: %s\n", ctx.App.Version, commit, date)) + r.PrintTextf("osv-scanner version: %s\ncommit: %s\nbuilt at: %s\n", ctx.App.Version, commit, date) } osv.RequestUserAgent = "osv-scanner/" + version.OSVVersion @@ -185,7 +185,7 @@ func run(args []string, stdout, stderr io.Writer) int { var callAnalysisStates map[string]bool if context.IsSet("experimental-call-analysis") { callAnalysisStates = createCallAnalysisStates([]string{"all"}, context.StringSlice("no-call-analysis")) - r.PrintText("Warning: the experimental-call-analysis flag has been replaced. Please use the call-analysis and no-call-analysis flags instead.\n") + r.PrintTextf("Warning: the experimental-call-analysis flag has been replaced. Please use the call-analysis and no-call-analysis flags instead.\n") } else { callAnalysisStates = createCallAnalysisStates(context.StringSlice("call-analysis"), context.StringSlice("no-call-analysis")) } @@ -236,10 +236,10 @@ func run(args []string, stdout, stderr io.Writer) int { case errors.Is(err, osvscanner.VulnerabilitiesFoundErr): return 1 case errors.Is(err, osvscanner.NoPackagesFoundErr): - r.PrintError("No package sources found, --help for usage information.\n") + r.PrintErrorf("No package sources found, --help for usage information.\n") return 128 } - r.PrintError(fmt.Sprintf("%v\n", err)) + r.PrintErrorf("%v\n", err) } // if we've been told to print an error, and not already exited with diff --git a/cmd/osv-scanner/main_test.go b/cmd/osv-scanner/main_test.go index 05afb811b75..40dc62fe6ef 100644 --- a/cmd/osv-scanner/main_test.go +++ b/cmd/osv-scanner/main_test.go @@ -623,7 +623,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) { args: []string{"", "--lockfile=go.mod:./fixtures/locks-many/replace-local.mod"}, wantExitCode: 0, wantStdout: ` - Scanned /fixtures/locks-many/replace-local.mod file as a go.mod and found 2 packages + Scanned /fixtures/locks-many/replace-local.mod file as a go.mod and found 1 package Filtered 1 local package/s from the scan. No issues found `, diff --git a/docs/github-action.md b/docs/github-action.md index 5e899749327..36fc1a6506d 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -20,18 +20,16 @@ nav_order: 7 OSV-Scanner is offered as a GitHub Action. We currently have two different GitHub Actions: -1. An action that triggers a scan with each [pull request](./github-action.md#scans-on-prs) and will only check for new vulnerabilities introduced through the pull request. -2. An action that performs a single vulnerability scan, which can be configured to scan on a [regular schedule](./github-action.md#scheduled-scans), or used as a check [on releases](./github-action.md#scan-on-release) to prevent releasing with known vulnerabilities in dependencies. +1. An action that triggers a scan with each [pull request](./github-action.md#scan-on-pull-request) and will only report new vulnerabilities introduced through the pull request. +2. An action that performs a full vulnerability scan, which can be configured to scan on a [regular schedule](./github-action.md#scheduled-scans). The full vulnerability scan can also be configured to run [on release](./github-action.md#scan-on-release) to prevent releasing with known vulnerabilities in dependencies. -## Scans on PRs +## Scan on pull request -Scanning your project on each pull request can help you keep vulnerabilities out of your project. This GitHub Action compares a vulnerability scan of the target branch to a vulnerability scan of the feature branch, and will fail if there are new vulnerabilities found which doesn't exist in the target branch. You will be notified of any new vulnerabilities introduced through the feature branch. You can also choose to [prevent merging](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging) if new vulnerabilities are introduced through the feature branch. +Scanning your project on each pull request can help you keep vulnerabilities out of your project. This GitHub Action compares a vulnerability scan of the target branch to a vulnerability scan of the feature branch, and will fail if there are new vulnerabilities introduced through the feature branch. You may choose to [prevent merging](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging) if new vulnerabilities are introduced, but by default the check will only warn users. ### Instructions -In your project repository, create a new file `.github/workflows/osv-scanner-pr.yml`. - -Include the following in the [`osv-scanner-pr.yml`](https://github.com/google/osv-scanner/blob/main/.github/workflows/osv-scanner-pr.yml) file: +In your project repository, create a new file `.github/workflows/osv-scanner-pr.yml` and include the following: ```yml name: OSV-Scanner PR Scan @@ -51,104 +49,20 @@ permissions: jobs: scan-pr: - uses: "google/osv-scanner/.github/workflows/osv-scanner-reusable-pr.yml@main" + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.5.0" ``` ### View results Results may be viewed by clicking on the details of the failed action, either from your project's actions tab or directly on the PR. Results are also included in GitHub annotations on the "Files changed" tab for the PR. -### Customization - -`osv-scanner-reusable.yml` takes two optional inputs: - -- `scan-args`: This value is passed to `osv-scanner` CLI after being split by each line. See the [usage](./usage) page for the available options. - Importantly `--format` and `--output` flags are already set by the reusable workflow and should not be overridden here. - Default: - ```bash - --recursive # Recursively scan subdirectories - --skip-git=true # Skip commit scanning to focus on dependencies - ./ # Start the scan from the root of the repository - ``` -- `results-file-name`: This is the name of the final SARIF file uploaded to Github. - Default: `results.sarif` -- `download-artifact`: Optional artifact to download for scanning. Can be used if you need to do some preprocessing to prepare the lockfiles for scanning. - If the file names in the artifact are not standard lockfile names, make sure to add custom scan-args to specify the lockfile type and path (see [specify lockfiles](./usage#specify-lockfiles)). -- `upload-sarif`: Whether to upload the results to Security > Code Scanning. Defaults to `true`. - -
- -Examples - - -##### Scan specific lockfiles - -```yml -jobs: - scan-pr: - uses: "google/osv-scanner/.github/workflows/osv-scanner-reusable.yml" - with: - scan-args: |- - --lockfile=./path/to/lockfile1 - --lockfile=requirements.txt:./path/to/python-lockfile2.txt -``` - -##### Default arguments - -```yml -jobs: - scan-pr: - uses: "google/osv-scanner/.github/workflows/osv-scanner-reusable.yml" - with: - scan-args: |- - --recursive - --skip-git=true - ./ -``` - -##### Using download-artifact input to support preprocessing - -```yml -jobs: - extract-deps: - name: Extract Dependencies - # ... - steps: - # ... Steps to extract your dependencies - - name: "upload osv-scanner deps" # Upload the deps - uses: actions/upload-artifact@v4 - with: - name: converted-OSV-Scanner-deps - path: osv-scanner-deps.json - retention-days: 2 - vuln-scan: - name: Vulnerability scanning - # makes sure the extraction step is completed before running the scanner - needs: extract-deps - uses: "google/osv-scanner/.github/workflows/osv-scanner-reusable.yml@main" - with: - # Download the artifact uploaded in extract-deps step - download-artifact: converted-OSV-Scanner-deps - # Scan only the file inside the uploaded artifact - scan-args: |- - --lockfile=osv-scanner:osv-scanner-deps.json - permissions: - # Needed to upload the SARIF results to code-scanning dashboard. - security-events: write - contents: read -``` - -
- ## Scheduled scans Regularly scanning your project for vulnerabilities can alert you to new vulnerabilities in your dependency tree. This GitHub Action will scan your project on a set schedule and report all known vulnerabilities. If vulnerabilities are found the action will return a failed status. ### Instructions -In your project repository, create a new file `.github/workflows/osv-scanner-scheduled.yml`. - -Include the following in the [`osv-scanner-scheduled.yml`](https://github.com/google/osv-scanner/blob/main/.github/workflows/osv-scanner-scheduled.yml) file: +In your project repository, create a new file `.github/workflows/osv-scanner-scheduled.yml` and include the following: ```yml name: OSV-Scanner Scheduled Scan @@ -168,15 +82,11 @@ permissions: jobs: scan-scheduled: - uses: "google/osv-scanner/.github/workflows/osv-scanner-reusable.yml@main" + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.5.0" ``` As written, the scanner will run on 12:30 pm UTC every Monday, and also on every push to the main branch. You can change the schedule by following the instructions [here](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule). -### Customization - -`osv-scanner-reusable-pr.yml` has the same customization options as `osv-scanner-reusable.yml`, which is described [here](./github-action.md#customization). - ### View results Maintainers can review results of the scan by navigating to their project's `security > code scanning` tab. Vulnerability details can also be viewed by clicking on the details of the failed action. @@ -223,3 +133,83 @@ jobs: ### View results Results may be viewed by clicking on the details of the failed release action from the action tab. + +## Customization + +The GitHub Actions have the following optional inputs: + +- `scan-args`: This value is passed to `osv-scanner` CLI after being split by each line. See the [usage](./usage) page for the available options. The `--format` and `--output` flags are already set by the reusable workflow and should not be overridden here. + Default: + ```bash + --recursive # Recursively scan subdirectories + --skip-git=true # Skip commit scanning to focus on dependencies + ./ # Start the scan from the root of the repository + ``` +- `results-file-name`: This is the name of the final SARIF file uploaded to Github. + Default: `results.sarif` +- `download-artifact`: Optional artifact to download for scanning. Can be used if you need to do some preprocessing to prepare the lockfiles for scanning. If the file names in the artifact are not standard lockfile names, make sure to add custom scan-args to specify the lockfile type and path (see [specify lockfiles](./usage#specify-lockfiles)). +- `upload-sarif`: Whether to upload the results to Security > Code Scanning. Defaults to `true`. + +
+ +Examples + + +#### Scan specific lockfiles + +```yml +jobs: + scan-pr: + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.5.0" + with: + scan-args: |- + --lockfile=./path/to/lockfile1 + --lockfile=requirements.txt:./path/to/python-lockfile2.txt +``` + +#### Default arguments + +```yml +jobs: + scan-pr: + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.5.0" + with: + scan-args: |- + --recursive + --skip-git=true + ./ +``` + +#### Using download-artifact input to support preprocessing + +```yml +jobs: + extract-deps: + name: Extract Dependencies + # ... + steps: + # ... Steps to extract your dependencies + - name: "upload osv-scanner deps" # Upload the deps + uses: actions/upload-artifact@v4 + with: + name: converted-OSV-Scanner-deps + path: osv-scanner-deps.json + retention-days: 2 + vuln-scan: + name: Vulnerability scanning + # makes sure the extraction step is completed before running the scanner + needs: extract-deps + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.5.0" + with: + # Download the artifact uploaded in extract-deps step + download-artifact: converted-OSV-Scanner-deps + # Scan only the file inside the uploaded artifact + scan-args: |- + --lockfile=osv-scanner:osv-scanner-deps.json + permissions: + # Needed to upload the SARIF results to code-scanning dashboard. + security-events: write + contents: read +``` + +
diff --git a/go.mod b/go.mod index 51988086e4c..e9cfab0c71b 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.21 require ( deps.dev/api/v3alpha v0.0.0-20231114023923-e40c4d5c34e5 github.com/BurntSushi/toml v1.3.2 - github.com/CycloneDX/cyclonedx-go v0.7.2 + github.com/CycloneDX/cyclonedx-go v0.8.0 github.com/go-git/go-billy/v5 v5.5.0 - github.com/go-git/go-git/v5 v5.10.1 + github.com/go-git/go-git/v5 v5.11.0 github.com/google/go-cmp v0.6.0 github.com/hexops/gotextdiff v1.0.3 github.com/ianlancetaylor/demangle v0.0.0-20231023195312-e2daf7ba7156 diff --git a/go.sum b/go.sum index 672c80051cd..059722f84b3 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ deps.dev/api/v3alpha v0.0.0-20231114023923-e40c4d5c34e5 h1:Vvh14FIzt0+LaLWn2l09F deps.dev/api/v3alpha v0.0.0-20231114023923-e40c4d5c34e5/go.mod h1:uRN72FJn1F0FD/2ZYUOqdyFMu8VUsyHxvmZAMW30/DA= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/CycloneDX/cyclonedx-go v0.7.2 h1:kKQ0t1dPOlugSIYVOMiMtFqeXI2wp/f5DBIdfux8gnQ= -github.com/CycloneDX/cyclonedx-go v0.7.2/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk= +github.com/CycloneDX/cyclonedx-go v0.8.0 h1:FyWVj6x6hoJrui5uRQdYZcSievw3Z32Z88uYzG/0D6M= +github.com/CycloneDX/cyclonedx-go v0.8.0/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -43,8 +43,8 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+ github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.10.1 h1:tu8/D8i+TWxgKpzQ3Vc43e+kkhXqtsZCKI/egajKnxk= -github.com/go-git/go-git/v5 v5.10.1/go.mod h1:uEuHjxkHap8kAl//V5F/nNWwqIYtP/402ddd05mp0wg= +github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/internal/local/check.go b/internal/local/check.go index 9d655fb0857..8a5f9e87b94 100644 --- a/internal/local/check.go +++ b/internal/local/check.go @@ -108,7 +108,7 @@ func MakeRequest(r reporter.Reporter, query osv.BatchedQuery, offline bool, loca return nil, err } - r.PrintText(fmt.Sprintf("Loaded %s local db from %s\n", db.Name, db.StoredAt)) + r.PrintTextf("Loaded %s local db from %s\n", db.Name, db.StoredAt) dbs[ecosystem] = db @@ -120,7 +120,7 @@ func MakeRequest(r reporter.Reporter, query osv.BatchedQuery, offline bool, loca if err != nil { // currently, this will actually only error if the PURL cannot be parses - r.PrintError(fmt.Sprintf("skipping %s as it is not a valid PURL: %v\n", query.Package.PURL, err)) + r.PrintErrorf("skipping %s as it is not a valid PURL: %v\n", query.Package.PURL, err) results = append(results, osv.Response{Vulns: []models.Vulnerability{}}) continue @@ -134,7 +134,7 @@ func MakeRequest(r reporter.Reporter, query osv.BatchedQuery, offline bool, loca // Is a commit based query, skip local scanning results = append(results, osv.Response{}) - r.PrintText(fmt.Sprintf("Skipping commit scanning for: %s\n", pkg.Commit)) + r.PrintTextf("Skipping commit scanning for: %s\n", pkg.Commit) continue } @@ -143,7 +143,7 @@ func MakeRequest(r reporter.Reporter, query osv.BatchedQuery, offline bool, loca if err != nil { // currently, this will actually only error if the PURL cannot be parses - r.PrintError(fmt.Sprintf("could not load db for %s ecosystem: %v\n", pkg.Ecosystem, err)) + r.PrintErrorf("could not load db for %s ecosystem: %v\n", pkg.Ecosystem, err) results = append(results, osv.Response{Vulns: []models.Vulnerability{}}) continue diff --git a/internal/semantic/compare_test.go b/internal/semantic/compare_test.go index 99ed53b4156..16ea6951034 100644 --- a/internal/semantic/compare_test.go +++ b/internal/semantic/compare_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/google/osv-scanner/internal/semantic" + "github.com/google/osv-scanner/pkg/models" ) func expectedResult(t *testing.T, comparator string) int { @@ -43,7 +44,7 @@ func compareWord(t *testing.T, result int) string { } } -func runAgainstEcosystemFixture(t *testing.T, ecosystem semantic.Ecosystem, filename string) { +func runAgainstEcosystemFixture(t *testing.T, ecosystem models.Ecosystem, filename string) { t.Helper() file, err := os.Open("fixtures/" + filename) @@ -90,7 +91,7 @@ func runAgainstEcosystemFixture(t *testing.T, ecosystem semantic.Ecosystem, file } } -func parseAsVersion(t *testing.T, str string, ecosystem semantic.Ecosystem) semantic.Version { +func parseAsVersion(t *testing.T, str string, ecosystem models.Ecosystem) semantic.Version { t.Helper() v, err := semantic.Parse(str, ecosystem) @@ -104,7 +105,7 @@ func parseAsVersion(t *testing.T, str string, ecosystem semantic.Ecosystem) sema func expectCompareResult( t *testing.T, - ecosystem semantic.Ecosystem, + ecosystem models.Ecosystem, a string, b string, expectedResult int, @@ -130,7 +131,7 @@ func expectCompareResult( func expectEcosystemCompareResult( t *testing.T, - ecosystem semantic.Ecosystem, + ecosystem models.Ecosystem, a string, c string, b string, @@ -231,7 +232,7 @@ func TestVersion_Compare_Ecosystems(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - runAgainstEcosystemFixture(t, semantic.Ecosystem(tt.name), tt.file) + runAgainstEcosystemFixture(t, models.Ecosystem(tt.name), tt.file) }) } } diff --git a/internal/semantic/parse.go b/internal/semantic/parse.go index e19ec609878..029a1f155d6 100644 --- a/internal/semantic/parse.go +++ b/internal/semantic/parse.go @@ -3,11 +3,13 @@ package semantic import ( "errors" "fmt" + + "github.com/google/osv-scanner/pkg/models" ) var ErrUnsupportedEcosystem = errors.New("unsupported ecosystem") -func MustParse(str string, ecosystem Ecosystem) Version { +func MustParse(str string, ecosystem models.Ecosystem) Version { v, err := Parse(str, ecosystem) if err != nil { @@ -17,7 +19,7 @@ func MustParse(str string, ecosystem Ecosystem) Version { return v } -func Parse(str string, ecosystem Ecosystem) (Version, error) { +func Parse(str string, ecosystem models.Ecosystem) (Version, error) { //nolint:exhaustive // Using strings to specify ecosystem instead of lockfile types switch ecosystem { case "npm": diff --git a/internal/semantic/parse_test.go b/internal/semantic/parse_test.go index 8fcea3f8ded..513af3d3124 100644 --- a/internal/semantic/parse_test.go +++ b/internal/semantic/parse_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/osv-scanner/internal/semantic" "github.com/google/osv-scanner/pkg/lockfile" + "github.com/google/osv-scanner/pkg/models" ) func TestParse(t *testing.T) { @@ -17,7 +18,7 @@ func TestParse(t *testing.T) { ecosystems = append(ecosystems, "CRAN") for _, ecosystem := range ecosystems { - _, err := semantic.Parse("", ecosystem) + _, err := semantic.Parse("", models.Ecosystem(ecosystem)) if errors.Is(err, semantic.ErrUnsupportedEcosystem) { t.Errorf("'%s' is not a supported ecosystem", ecosystem) @@ -40,7 +41,7 @@ func TestMustParse(t *testing.T) { ecosystems = append(ecosystems, "CRAN") for _, ecosystem := range ecosystems { - semantic.MustParse("", ecosystem) + semantic.MustParse("", models.Ecosystem(ecosystem)) } } diff --git a/internal/semantic/types.go b/internal/semantic/types.go deleted file mode 100644 index e5a8444e22f..00000000000 --- a/internal/semantic/types.go +++ /dev/null @@ -1,5 +0,0 @@ -package semantic - -import "github.com/google/osv-scanner/pkg/lockfile" - -type Ecosystem = lockfile.Ecosystem diff --git a/internal/sourceanalysis/go.go b/internal/sourceanalysis/go.go index d4b86235a53..ea2afe3ac4b 100644 --- a/internal/sourceanalysis/go.go +++ b/internal/sourceanalysis/go.go @@ -21,7 +21,7 @@ func goAnalysis(r reporter.Reporter, pkgs []models.PackageVulns, source models.S cmd := exec.Command("go", "version") _, err := cmd.Output() if err != nil { - r.PrintText("Skipping call analysis on Go code since Go is not installed.\n") + r.PrintTextf("Skipping call analysis on Go code since Go is not installed.\n") return } @@ -29,9 +29,10 @@ func goAnalysis(r reporter.Reporter, pkgs []models.PackageVulns, source models.S res, err := runGovulncheck(filepath.Dir(source.Path), vulns) if err != nil { // TODO: Better method to identify the type of error and give advice specific to the error - r.PrintError( - fmt.Sprintf("Failed to run code analysis (govulncheck) on '%s' because %s\n"+ - "(the Go toolchain is required)\n", source.Path, err.Error())) + r.PrintErrorf( + "Failed to run code analysis (govulncheck) on '%s' because %s\n"+ + "(the Go toolchain is required)\n", source.Path, err.Error(), + ) return } diff --git a/internal/sourceanalysis/rust.go b/internal/sourceanalysis/rust.go index ccdabdcf1e2..454035eb3a6 100644 --- a/internal/sourceanalysis/rust.go +++ b/internal/sourceanalysis/rust.go @@ -34,7 +34,7 @@ const ( func rustAnalysis(r reporter.Reporter, pkgs []models.PackageVulns, source models.SourceInfo) { binaryPaths, err := rustBuildSource(r, source) if err != nil { - r.PrintError(fmt.Sprintf("failed to build cargo/rust project from source: %s\n", err)) + r.PrintErrorf("failed to build cargo/rust project from source: %s\n", err) return } @@ -50,14 +50,14 @@ func rustAnalysis(r reporter.Reporter, pkgs []models.PackageVulns, source models // Is a library, so need an extra step to extract the object binary file before passing to parseDWARFData buf, err := extractRlibArchive(path) if err != nil { - r.PrintError(fmt.Sprintf("failed to analyse '%s': %s\n", path, err)) + r.PrintErrorf("failed to analyse '%s': %s\n", path, err) continue } readAt = bytes.NewReader(buf.Bytes()) } else { f, err := os.Open(path) if err != nil { - r.PrintError(fmt.Sprintf("failed to read binary '%s': %s\n", path, err)) + r.PrintErrorf("failed to read binary '%s': %s\n", path, err) continue } // This is fine to defer til the end of the function as there's @@ -68,7 +68,7 @@ func rustAnalysis(r reporter.Reporter, pkgs []models.PackageVulns, source models calls, err := functionsFromDWARF(readAt) if err != nil { - r.PrintError(fmt.Sprintf("failed to analyse '%s': %s\n", path, err)) + r.PrintErrorf("failed to analyse '%s': %s\n", path, err) continue } @@ -233,11 +233,11 @@ func rustBuildSource(r reporter.Reporter, source models.SourceInfo) ([]string, e cmd.Stdout = &stdoutBuffer cmd.Stderr = &stderrBuffer - r.PrintText("Begin building rust/cargo project\n") + r.PrintTextf("Begin building rust/cargo project\n") if err := cmd.Run(); err != nil { - r.PrintError(fmt.Sprintf("cargo stdout:\n%s", stdoutBuffer.String())) - r.PrintError(fmt.Sprintf("cargo stderr:\n%s", stderrBuffer.String())) + r.PrintErrorf("cargo stdout:\n%s", stdoutBuffer.String()) + r.PrintErrorf("cargo stderr:\n%s", stderrBuffer.String()) return nil, fmt.Errorf("failed to run `%v`: %w", cmd.String(), err) } diff --git a/internal/utility/vulns/vulnerability.go b/internal/utility/vulns/vulnerability.go index 269ba6bfee8..f35811e37b3 100644 --- a/internal/utility/vulns/vulnerability.go +++ b/internal/utility/vulns/vulnerability.go @@ -40,7 +40,7 @@ func rangeContainsVersion(ar models.Range, pkg lockfile.PackageDetails) bool { return false } - vp := semantic.MustParse(pkg.Version, pkg.CompareAs) + vp := semantic.MustParse(pkg.Version, models.Ecosystem(pkg.CompareAs)) sort.Slice(ar.Events, func(i, j int) bool { a := ar.Events[i] @@ -54,7 +54,7 @@ func rangeContainsVersion(ar models.Range, pkg lockfile.PackageDetails) bool { return false } - return semantic.MustParse(eventVersion(a), pkg.CompareAs).CompareStr(eventVersion(b)) < 0 + return semantic.MustParse(eventVersion(a), models.Ecosystem(pkg.CompareAs)).CompareStr(eventVersion(b)) < 0 }) var affected bool diff --git a/pkg/config/config.go b/pkg/config/config.go index 8c4ce88cd19..cdf50da8946 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -72,7 +72,7 @@ func (c *ConfigManager) Get(r reporter.Reporter, targetPath string) Config { if err != nil { // TODO: This can happen when target is not a file (e.g. Docker container, git hash...etc.) // Figure out a more robust way to load config from non files - // r.PrintError(fmt.Sprintf("Can't find config path: %s\n", err)) + // r.PrintErrorf("Can't find config path: %s\n", err) return Config{} } @@ -83,7 +83,7 @@ func (c *ConfigManager) Get(r reporter.Reporter, targetPath string) Config { config, configErr := tryLoadConfig(configPath) if configErr == nil { - r.PrintText(fmt.Sprintf("Loaded filter from: %s\n", config.LoadPath)) + r.PrintTextf("Loaded filter from: %s\n", config.LoadPath) } else { // If config doesn't exist, use the default config config = c.DefaultConfig diff --git a/pkg/lockfile/ecosystems.go b/pkg/lockfile/ecosystems.go index b6dbd96a2c9..3f6583456db 100644 --- a/pkg/lockfile/ecosystems.go +++ b/pkg/lockfile/ecosystems.go @@ -1,5 +1,7 @@ package lockfile +// KnownEcosystems returns a list of ecosystems that `lockfile` supports +// automatically inferring an extractor for based on a file path. func KnownEcosystems() []Ecosystem { return []Ecosystem{ NpmEcosystem, diff --git a/pkg/lockfile/parse-go-lock.go b/pkg/lockfile/parse-go-lock.go index 396e3da2791..3e0eb4baae6 100644 --- a/pkg/lockfile/parse-go-lock.go +++ b/pkg/lockfile/parse-go-lock.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/google/osv-scanner/internal/semantic" "golang.org/x/mod/modfile" ) @@ -82,6 +83,24 @@ func (e GoLockExtractor) Extract(f DepFile) ([]PackageDetails, error) { } } + if parsedLockfile.Go != nil && parsedLockfile.Go.Version != "" { + v := semantic.ParseSemverLikeVersion(parsedLockfile.Go.Version, 3) + + goVersion := fmt.Sprintf( + "%d.%d.%d", + v.Components.Fetch(0), + v.Components.Fetch(1), + v.Components.Fetch(2), + ) + + packages["stdlib"] = PackageDetails{ + Name: "stdlib", + Version: goVersion, + Ecosystem: GoEcosystem, + CompareAs: GoEcosystem, + } + } + return pkgDetailsMapToSlice(deduplicatePackages(packages)), nil } diff --git a/pkg/lockfile/parse-go-lock_test.go b/pkg/lockfile/parse-go-lock_test.go index f1de2ae86c3..59e54d80796 100644 --- a/pkg/lockfile/parse-go-lock_test.go +++ b/pkg/lockfile/parse-go-lock_test.go @@ -130,6 +130,12 @@ func TestParseGoLock_TwoPackages(t *testing.T) { Ecosystem: lockfile.GoEcosystem, CompareAs: lockfile.GoEcosystem, }, + { + Name: "stdlib", + Version: "1.17.0", + Ecosystem: lockfile.GoEcosystem, + CompareAs: lockfile.GoEcosystem, + }, }) } @@ -173,6 +179,12 @@ func TestParseGoLock_IndirectPackages(t *testing.T) { Ecosystem: lockfile.GoEcosystem, CompareAs: lockfile.GoEcosystem, }, + { + Name: "stdlib", + Version: "1.17.0", + Ecosystem: lockfile.GoEcosystem, + CompareAs: lockfile.GoEcosystem, + }, }) } diff --git a/pkg/osvscanner/optional_enricher.go b/pkg/osvscanner/optional_enricher.go deleted file mode 100644 index 23056052a75..00000000000 --- a/pkg/osvscanner/optional_enricher.go +++ /dev/null @@ -1,42 +0,0 @@ -package osvscanner - -import ( - "fmt" - "os/exec" - "strings" - - "github.com/google/osv-scanner/pkg/lockfile" - "github.com/google/osv-scanner/pkg/reporter" -) - -// Get the go version of the default environment that osv-scanner is being ran on -func getGoVersion() (string, error) { - versionBytes, err := exec.Command("go", "env", "GOVERSION").Output() - if err != nil { - return "", err - } - // version format can be: - // - go1.20.6 - // - go1.22-20230729-RC00 cl/552016856 +457721cd52 X:fieldtrack,boringcrypto - version := strings.TrimPrefix(string(versionBytes), "go") - version, _, _ = strings.Cut(version, " ") - - return version, nil -} - -func addCompilerVersion(r reporter.Reporter, parsedLockfile *lockfile.Lockfile) { - switch parsedLockfile.ParsedAs { //nolint:gocritic - case "go.mod": - goVer, err := getGoVersion() - if err != nil { - r.PrintText(fmt.Sprintf("cannot get go standard library version, go might not be installed: %s\n", err)) - } else { - parsedLockfile.Packages = append(parsedLockfile.Packages, lockfile.PackageDetails{ - Name: "stdlib", - Version: goVer, - Ecosystem: lockfile.GoEcosystem, - CompareAs: lockfile.GoEcosystem, - }) - } - } -} diff --git a/pkg/osvscanner/osvscanner.go b/pkg/osvscanner/osvscanner.go index 662311432a0..bf4c77c071a 100644 --- a/pkg/osvscanner/osvscanner.go +++ b/pkg/osvscanner/osvscanner.go @@ -103,7 +103,7 @@ func scanDir(r reporter.Reporter, dir string, skipGit bool, recursive bool, useG var err error ignoreMatcher, err = parseGitIgnores(dir) if err != nil { - r.PrintError(fmt.Sprintf("Unable to parse git ignores: %v\n", err)) + r.PrintErrorf("Unable to parse git ignores: %v\n", err) useGitIgnore = false } } @@ -114,24 +114,24 @@ func scanDir(r reporter.Reporter, dir string, skipGit bool, recursive bool, useG return scannedPackages, filepath.WalkDir(dir, func(path string, info os.DirEntry, err error) error { if err != nil { - r.PrintText(fmt.Sprintf("Failed to walk %s: %v\n", path, err)) + r.PrintTextf("Failed to walk %s: %v\n", path, err) return err } path, err = filepath.Abs(path) if err != nil { - r.PrintError(fmt.Sprintf("Failed to walk path %s\n", err)) + r.PrintErrorf("Failed to walk path %s\n", err) return err } if useGitIgnore { match, err := ignoreMatcher.match(path, info.IsDir()) if err != nil { - r.PrintText(fmt.Sprintf("Failed to resolve gitignore for %s: %v\n", path, err)) + r.PrintTextf("Failed to resolve gitignore for %s: %v\n", path, err) // Don't skip if we can't parse now - potentially noisy for directories with lots of items } else if match { if root { // Don't silently skip if the argument file was ignored. - r.PrintError(fmt.Sprintf("%s was not scanned because it is excluded by a .gitignore file. Use --no-ignore to scan it.\n", path)) + r.PrintErrorf("%s was not scanned because it is excluded by a .gitignore file. Use --no-ignore to scan it.\n", path) } if info.IsDir() { return filepath.SkipDir @@ -144,7 +144,7 @@ func scanDir(r reporter.Reporter, dir string, skipGit bool, recursive bool, useG if !skipGit && info.IsDir() && info.Name() == ".git" { pkgs, err := scanGit(r, filepath.Dir(path)+"/") if err != nil { - r.PrintText(fmt.Sprintf("scan failed for git repository, %s: %v\n", path, err)) + r.PrintTextf("scan failed for git repository, %s: %v\n", path, err) // Not fatal, so don't return and continue scanning other files } scannedPackages = append(scannedPackages, pkgs...) @@ -156,7 +156,7 @@ func scanDir(r reporter.Reporter, dir string, skipGit bool, recursive bool, useG if extractor, _ := lockfile.FindExtractor(path, ""); extractor != nil { pkgs, err := scanLockfile(r, path, "") if err != nil { - r.PrintError(fmt.Sprintf("Attempted to scan lockfile but failed: %s\n", path)) + r.PrintErrorf("Attempted to scan lockfile but failed: %s\n", path) } scannedPackages = append(scannedPackages, pkgs...) } @@ -171,7 +171,7 @@ func scanDir(r reporter.Reporter, dir string, skipGit bool, recursive bool, useG if _, ok := vendoredLibNames[strings.ToLower(filepath.Base(path))]; ok { pkgs, err := scanDirWithVendoredLibs(r, path) if err != nil { - r.PrintText(fmt.Sprintf("scan failed for dir containing vendored libs %s: %v\n", path, err)) + r.PrintTextf("scan failed for dir containing vendored libs %s: %v\n", path, err) } scannedPackages = append(scannedPackages, pkgs...) } @@ -281,7 +281,7 @@ func queryDetermineVersions(repoDir string) (*osv.DetermineVersionResponse, erro } func scanDirWithVendoredLibs(r reporter.Reporter, path string) ([]scannedPackage, error) { - r.PrintText(fmt.Sprintf("Scanning directory for vendored libs: %s\n", path)) + r.PrintTextf("Scanning directory for vendored libs: %s\n", path) entries, err := os.ReadDir(path) if err != nil { return nil, err @@ -295,17 +295,17 @@ func scanDirWithVendoredLibs(r reporter.Reporter, path string) ([]scannedPackage libPath := filepath.Join(path, entry.Name()) - r.PrintText(fmt.Sprintf("Scanning potential vendored dir: %s\n", libPath)) + r.PrintTextf("Scanning potential vendored dir: %s\n", libPath) // TODO: make this a goroutine to parallelise this operation results, err := queryDetermineVersions(libPath) if err != nil { - r.PrintText(fmt.Sprintf("Error scanning sub-directory '%s' with error: %v", libPath, err)) + r.PrintTextf("Error scanning sub-directory '%s' with error: %v", libPath, err) continue } if len(results.Matches) > 0 && results.Matches[0].Score > determineVersionThreshold { match := results.Matches[0] - r.PrintText(fmt.Sprintf("Identified %s as %s at %s.\n", libPath, match.RepoInfo.Address, match.RepoInfo.Commit)) + r.PrintTextf("Identified %s as %s at %s.\n", libPath, match.RepoInfo.Address, match.RepoInfo.Commit) packages = append(packages, createCommitQueryPackage(match.RepoInfo.Commit, libPath)) } } @@ -357,21 +357,19 @@ func scanLockfile(r reporter.Reporter, path string, parseAs string) ([]scannedPa return nil, err } - addCompilerVersion(r, &parsedLockfile) - parsedAsComment := "" if parseAs != "" { parsedAsComment = fmt.Sprintf("as a %s ", parseAs) } - r.PrintText(fmt.Sprintf( + r.PrintTextf( "Scanned %s file %sand found %d %s\n", path, parsedAsComment, len(parsedLockfile.Packages), output.Form(len(parsedLockfile.Packages), "package", "packages"), - )) + ) packages := make([]scannedPackage, len(parsedLockfile.Packages)) for i, pkgDetail := range parsedLockfile.Packages { @@ -445,19 +443,19 @@ func scanSBOMFile(r reporter.Reporter, path string, fromFSScan bool) ([]scannedP continue } - r.PrintText(fmt.Sprintf( + r.PrintTextf( "Scanned %s as %s SBOM and found %d %s\n", path, provider.Name(), len(packages), output.Form(len(packages), "package", "packages"), - )) + ) if ignoredCount > 0 { - r.PrintText(fmt.Sprintf( + r.PrintTextf( "Ignored %d %s with invalid PURLs\n", ignoredCount, output.Form(ignoredCount, "package", "packages"), - )) + ) } return packages, nil @@ -474,9 +472,9 @@ func scanSBOMFile(r reporter.Reporter, path string, fromFSScan bool) ([]scannedP // Don't log these errors if we're coming from an FS scan, since it can get very noisy. if !fromFSScan { - r.PrintText("Failed to parse SBOM using all supported formats:\n") + r.PrintTextf("Failed to parse SBOM using all supported formats:\n") for _, err := range errs { - r.PrintText(err.Error() + "\n") + r.PrintTextf(err.Error() + "\n") } } @@ -526,7 +524,7 @@ func scanGit(r reporter.Reporter, repoDir string) ([]scannedPackage, error) { if err != nil { return nil, err } - r.PrintText(fmt.Sprintf("Scanning %s at commit %s\n", repoDir, commit)) + r.PrintTextf("Scanning %s at commit %s\n", repoDir, commit) //nolint:prealloc // Not sure how many there will be in advance. var packages []scannedPackage @@ -538,7 +536,7 @@ func scanGit(r reporter.Reporter, repoDir string) ([]scannedPackage, error) { } for _, s := range submodules { - r.PrintText(fmt.Sprintf("Scanning submodule %s at commit %s\n", s.Path, s.Expected.String())) + r.PrintTextf("Scanning submodule %s at commit %s\n", s.Path, s.Expected.String()) packages = append(packages, createCommitQueryPackage(s.Expected.String(), path.Join(repoDir, s.Path))) } @@ -560,12 +558,12 @@ func scanDebianDocker(r reporter.Reporter, dockerImageName string) ([]scannedPac stdout, err := cmd.StdoutPipe() if err != nil { - r.PrintError(fmt.Sprintf("Failed to get stdout: %s\n", err)) + r.PrintErrorf("Failed to get stdout: %s\n", err) return nil, err } err = cmd.Start() if err != nil { - r.PrintError(fmt.Sprintf("Failed to start docker image: %s\n", err)) + r.PrintErrorf("Failed to start docker image: %s\n", err) return nil, err } // TODO: Do error checking here @@ -581,7 +579,7 @@ func scanDebianDocker(r reporter.Reporter, dockerImageName string) ([]scannedPac } splitText := strings.Split(text, "###") if len(splitText) != 2 { - r.PrintError(fmt.Sprintf("Unexpected output from Debian container: \n\n%s\n", text)) + r.PrintErrorf("Unexpected output from Debian container: \n\n%s\n", text) return nil, fmt.Errorf("unexpected output from Debian container: \n\n%s", text) } // TODO(rexpan): Get and specify exact debian release version @@ -595,11 +593,11 @@ func scanDebianDocker(r reporter.Reporter, dockerImageName string) ([]scannedPac }, }) } - r.PrintText(fmt.Sprintf( + r.PrintTextf( "Scanned docker image with %d %s\n", len(packages), output.Form(len(packages), "package", "packages"), - )) + ) return packages, nil } @@ -645,11 +643,11 @@ func filterPackageVulns(r reporter.Reporter, pkgVulns models.PackageVulns, confi // NB: This only prints the first reason encountered in all the aliases. switch len(group.Aliases) { case 1: - r.PrintText(fmt.Sprintf("%s has been filtered out because: %s\n", ignoreLine.ID, ignoreLine.Reason)) + r.PrintTextf("%s has been filtered out because: %s\n", ignoreLine.ID, ignoreLine.Reason) case 2: - r.PrintText(fmt.Sprintf("%s and 1 alias have been filtered out because: %s\n", ignoreLine.ID, ignoreLine.Reason)) + r.PrintTextf("%s and 1 alias have been filtered out because: %s\n", ignoreLine.ID, ignoreLine.Reason) default: - r.PrintText(fmt.Sprintf("%s and %d aliases have been filtered out because: %s\n", ignoreLine.ID, len(group.Aliases)-1, ignoreLine.Reason)) + r.PrintTextf("%s and %d aliases have been filtered out because: %s\n", ignoreLine.ID, len(group.Aliases)-1, ignoreLine.Reason) } break @@ -725,7 +723,7 @@ func DoScan(actions ScannerActions, r reporter.Reporter) (models.VulnerabilityRe if actions.ConfigOverridePath != "" { err := configManager.UseOverride(actions.ConfigOverridePath) if err != nil { - r.PrintError(fmt.Sprintf("Failed to read config file: %s\n", err)) + r.PrintErrorf("Failed to read config file: %s\n", err) return models.VulnerabilityResults{}, err } } @@ -741,7 +739,7 @@ func DoScan(actions ScannerActions, r reporter.Reporter) (models.VulnerabilityRe parseAs, lockfilePath := parseLockfilePath(lockfileElem) lockfilePath, err := filepath.Abs(lockfilePath) if err != nil { - r.PrintError(fmt.Sprintf("Failed to resolved path with error %s\n", err)) + r.PrintErrorf("Failed to resolved path with error %s\n", err) return models.VulnerabilityResults{}, err } pkgs, err := scanLockfile(r, lockfilePath, parseAs) @@ -768,7 +766,7 @@ func DoScan(actions ScannerActions, r reporter.Reporter) (models.VulnerabilityRe } for _, dir := range actions.DirectoryPaths { - r.PrintText(fmt.Sprintf("Scanning dir %s\n", dir)) + r.PrintTextf("Scanning dir %s\n", dir) pkgs, err := scanDir(r, dir, actions.SkipGit, actions.Recursive, !actions.NoIgnore, actions.CompareOffline) if err != nil { return models.VulnerabilityResults{}, err @@ -783,7 +781,7 @@ func DoScan(actions ScannerActions, r reporter.Reporter) (models.VulnerabilityRe filteredScannedPackages := filterUnscannablePackages(scannedPackages) if len(filteredScannedPackages) != len(scannedPackages) { - r.PrintText(fmt.Sprintf("Filtered %d local package/s from the scan.\n", len(scannedPackages)-len(filteredScannedPackages))) + r.PrintTextf("Filtered %d local package/s from the scan.\n", len(scannedPackages)-len(filteredScannedPackages)) } vulnsResp, err := makeRequest(r, filteredScannedPackages, actions.CompareLocally, actions.CompareOffline, actions.LocalDBPath) @@ -802,11 +800,11 @@ func DoScan(actions ScannerActions, r reporter.Reporter) (models.VulnerabilityRe filtered := filterResults(r, &results, &configManager, actions.ShowAllPackages) if filtered > 0 { - r.PrintText(fmt.Sprintf( + r.PrintTextf( "Filtered %d %s from output\n", filtered, output.Form(filtered, "vulnerability", "vulnerabilities"), - )) + ) } if len(results.Results) > 0 { diff --git a/pkg/osvscanner/vulnerability_result.go b/pkg/osvscanner/vulnerability_result.go index b085682fc2e..4c9a45729d2 100644 --- a/pkg/osvscanner/vulnerability_result.go +++ b/pkg/osvscanner/vulnerability_result.go @@ -1,7 +1,6 @@ package osvscanner import ( - "fmt" "sort" "strings" @@ -38,8 +37,7 @@ func buildVulnerabilityResults( var err error pkg.Package, err = models.PURLToPackage(rawPkg.PURL) if err != nil { - r.PrintError(fmt.Sprintf("Failed to parse purl: %s, with error: %s", - rawPkg.PURL, err)) + r.PrintErrorf("Failed to parse purl: %s, with error: %s", rawPkg.PURL, err) continue } diff --git a/pkg/reporter/gh-annotations_reporter.go b/pkg/reporter/gh-annotations_reporter.go index 01ca072386c..034e4f19a1b 100644 --- a/pkg/reporter/gh-annotations_reporter.go +++ b/pkg/reporter/gh-annotations_reporter.go @@ -23,7 +23,11 @@ func NewGHAnnotationsReporter(stdout io.Writer, stderr io.Writer) *GHAnnotations } func (r *GHAnnotationsReporter) PrintError(msg string) { - fmt.Fprint(r.stderr, msg) + r.PrintErrorf(msg) +} + +func (r *GHAnnotationsReporter) PrintErrorf(msg string, a ...any) { + fmt.Fprintf(r.stderr, msg, a...) r.hasPrintedError = true } @@ -32,7 +36,11 @@ func (r *GHAnnotationsReporter) HasPrintedError() bool { } func (r *GHAnnotationsReporter) PrintText(msg string) { - fmt.Fprint(r.stderr, msg) + r.PrintTextf(msg) +} + +func (r *GHAnnotationsReporter) PrintTextf(msg string, a ...any) { + fmt.Fprintf(r.stderr, msg, a...) } func (r *GHAnnotationsReporter) PrintResult(vulnResult *models.VulnerabilityResults) error { diff --git a/pkg/reporter/json_reporter.go b/pkg/reporter/json_reporter.go index f47785fd6ff..96e870b975c 100644 --- a/pkg/reporter/json_reporter.go +++ b/pkg/reporter/json_reporter.go @@ -23,7 +23,11 @@ func NewJSONReporter(stdout io.Writer, stderr io.Writer) *JSONReporter { } func (r *JSONReporter) PrintError(msg string) { - fmt.Fprint(r.stderr, msg) + r.PrintErrorf(msg) +} + +func (r *JSONReporter) PrintErrorf(msg string, a ...any) { + fmt.Fprintf(r.stderr, msg, a...) r.hasPrintedError = true } @@ -32,8 +36,12 @@ func (r *JSONReporter) HasPrintedError() bool { } func (r *JSONReporter) PrintText(msg string) { + r.PrintTextf(msg) +} + +func (r *JSONReporter) PrintTextf(msg string, a ...any) { // Print non json text to stderr - fmt.Fprint(r.stderr, msg) + fmt.Fprintf(r.stderr, msg, a...) } func (r *JSONReporter) PrintResult(vulnResult *models.VulnerabilityResults) error { diff --git a/pkg/reporter/reporter.go b/pkg/reporter/reporter.go index d4f5e75d28a..5e9e63b83c7 100644 --- a/pkg/reporter/reporter.go +++ b/pkg/reporter/reporter.go @@ -5,15 +5,45 @@ import ( ) type Reporter interface { - // PrintError writes the given message to stderr, regardless of if the reporter - // is outputting as JSON or not + // PrintError prints errors in an appropriate manner to ensure that results + // are printed in a way that is semantically valid for the intended consumer, + // and tracking that an error has been printed. + // + // Where the error is actually printed (if at all) is entirely up to the actual + // reporter, though generally it will be to stderr. + // + // Deprecated: use PrintErrorf instead PrintError(msg string) + // PrintErrorf prints errors in an appropriate manner to ensure that results + // are printed in a way that is semantically valid for the intended consumer, + // and tracking that an error has been printed. + // + // Where the error is actually printed (if at all) is entirely up to the actual + // reporter, though generally it will be to stderr. + PrintErrorf(msg string, a ...any) + // HasPrintedError returns true if there have been any calls to PrintError or + // PrintErrorf. + // + // This does not actually represent if the error was actually printed anywhere + // since what happens to the error message is up to the actual reporter. HasPrintedError() bool - // PrintText writes the given message to stdout, _unless_ the reporter is set - // to output as JSON, in which case it writes the message to stderr. + // PrintText prints text in an appropriate manner to ensure that results + // are printed in a way that is semantically valid for the intended consumer. + // + // Where the text is actually printed (if at all) is entirely up to the actual + // reporter; in most cases for "human format" reporters this will be stdout + // whereas for "machine format" reporters this will stderr. // - // This should be used for content that should always be outputted, but that - // should not be captured when piping if outputting JSON. + // Deprecated: use PrintTextf instead PrintText(msg string) + // PrintTextf prints text in an appropriate manner to ensure that results + // are printed in a way that is semantically valid for the intended consumer. + // + // Where the text is actually printed (if at all) is entirely up to the actual + // reporter; in most cases for "human format" reporters this will be stdout + // whereas for "machine format" reporters this will stderr. + PrintTextf(msg string, a ...any) + // PrintResult prints the models.VulnerabilityResults per the logic of the + // actual reporter PrintResult(vulnResult *models.VulnerabilityResults) error } diff --git a/pkg/reporter/sarif_reporter.go b/pkg/reporter/sarif_reporter.go index 825237b03a0..2073bebe348 100644 --- a/pkg/reporter/sarif_reporter.go +++ b/pkg/reporter/sarif_reporter.go @@ -23,7 +23,11 @@ func NewSarifReporter(stdout io.Writer, stderr io.Writer) *SARIFReporter { } func (r *SARIFReporter) PrintError(msg string) { - fmt.Fprint(r.stderr, msg) + r.PrintErrorf(msg) +} + +func (r *SARIFReporter) PrintErrorf(msg string, a ...any) { + fmt.Fprintf(r.stderr, msg, a...) r.hasPrintedError = true } @@ -32,7 +36,11 @@ func (r *SARIFReporter) HasPrintedError() bool { } func (r *SARIFReporter) PrintText(msg string) { - fmt.Fprint(r.stderr, msg) + r.PrintTextf(msg) +} + +func (r *SARIFReporter) PrintTextf(msg string, a ...any) { + fmt.Fprintf(r.stderr, msg, a...) } func (r *SARIFReporter) PrintResult(vulnResult *models.VulnerabilityResults) error { diff --git a/pkg/reporter/table_reporter.go b/pkg/reporter/table_reporter.go index 5e159bcd03c..022dd1a17cc 100644 --- a/pkg/reporter/table_reporter.go +++ b/pkg/reporter/table_reporter.go @@ -28,7 +28,11 @@ func NewTableReporter(stdout io.Writer, stderr io.Writer, markdown bool, termina } func (r *TableReporter) PrintError(msg string) { - fmt.Fprint(r.stderr, msg) + r.PrintErrorf(msg) +} + +func (r *TableReporter) PrintErrorf(msg string, a ...any) { + fmt.Fprintf(r.stderr, msg, a...) r.hasPrintedError = true } @@ -37,7 +41,11 @@ func (r *TableReporter) HasPrintedError() bool { } func (r *TableReporter) PrintText(msg string) { - fmt.Fprint(r.stdout, msg) + r.PrintTextf(msg) +} + +func (r *TableReporter) PrintTextf(msg string, a ...any) { + fmt.Fprintf(r.stdout, msg, a...) } func (r *TableReporter) PrintResult(vulnResult *models.VulnerabilityResults) error { diff --git a/pkg/reporter/void_reporter.go b/pkg/reporter/void_reporter.go index c90f4552b88..443df3d240a 100644 --- a/pkg/reporter/void_reporter.go +++ b/pkg/reporter/void_reporter.go @@ -1,22 +1,32 @@ package reporter -import "github.com/google/osv-scanner/pkg/models" +import ( + "github.com/google/osv-scanner/pkg/models" +) type VoidReporter struct { hasPrintedError bool } +func (r *VoidReporter) PrintError(msg string) { + r.PrintErrorf(msg) +} + +func (r *VoidReporter) PrintErrorf(msg string, a ...any) { + r.hasPrintedError = true +} + func (r *VoidReporter) HasPrintedError() bool { return r.hasPrintedError } func (r *VoidReporter) PrintText(msg string) { + r.PrintTextf(msg) } -func (r *VoidReporter) PrintResult(vulnResult *models.VulnerabilityResults) error { - return nil +func (r *VoidReporter) PrintTextf(msg string, a ...any) { } -func (r *VoidReporter) PrintError(msg string) { - r.hasPrintedError = true +func (r *VoidReporter) PrintResult(vulnResult *models.VulnerabilityResults) error { + return nil }