From dc9a33233c35b1278b9ba185c76b1630901f4d36 Mon Sep 17 00:00:00 2001 From: Rex P <106129829+another-rex@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:26:47 +1100 Subject: [PATCH] chore: Merge V2 back into main (#1434) Now that the last release is out, the main branch should be the v2 branch, and we can continue developing on main. One caveat, scorecard github action has not updated yet to support the new ignore file format. --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com> Co-authored-by: Gareth Jones <Jones258@Gmail.com> Co-authored-by: Xueqin Cui <72771658+cuixq@users.noreply.github.com> Co-authored-by: Michael Kedar <michaelkedar@google.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Emmanuel Ferdman <emmanuelferdman@gmail.com> Co-authored-by: Ignacio Vazquez <ivaz@meta.com> Co-authored-by: Holly Gong <39108850+hogo6002@users.noreply.github.com> --- .github/workflows/checks.yml | 4 +- .github/workflows/codeql-analysis.yml | 4 +- .../workflows/osv-scanner-unified-action.yml | 4 +- .github/workflows/scorecards.yml | 2 +- .prettierignore | 1 + cmd/osv-scanner/__snapshots__/main_test.snap | 80 +- .../locks-requirements/my-requirements.txt | 2 +- .../locks-requirements/requirements-dev.txt | 2 +- .../locks-requirements/requirements.txt | 4 +- .../the_requirements_for_test.txt | 2 +- .../fixtures/sbom-insecure/osv-scanner.toml | 65 +- cmd/osv-scanner/main.go | 12 + cmd/osv-scanner/main_test.go | 58 +- cmd/osv-scanner/scan/main.go | 96 +- go.mod | 53 +- go.sum | 106 +- internal/image/__snapshots__/image_test.snap | 32 +- internal/image/extractor.go | 153 +- ...ine-release => alpine-3.18-alpine-release} | 0 .../image/fixtures/alpine-3.18-os-release | 7 + .../image/fixtures/test-alpine.Dockerfile | 5 +- .../test-node_modules-npm-empty.Dockerfile | 2 +- .../test-node_modules-npm-full.Dockerfile | 2 +- .../test-node_modules-pnpm-empty.Dockerfile | 2 +- .../test-node_modules-pnpm-full.Dockerfile | 2 +- .../test-node_modules-yarn-empty.Dockerfile | 2 +- .../test-node_modules-yarn-full.Dockerfile | 2 +- internal/image/image.go | 17 +- internal/image/image_test.go | 5 - internal/image/layer.go | 129 +- internal/image/pathtree/pathtree.go | 133 ++ internal/image/pathtree/pathtree_test.go | 264 ++++ internal/image/scan.go | 131 +- internal/lockfilescalibr/errors.go | 9 + .../language/java/pomxmlnet/extractor.go | 188 +++ .../language/java/pomxmlnet/extractor_test.go | 366 +++++ .../java/pomxmlnet/testdata/maven/empty.xml | 7 + .../testdata/maven/interpolation.xml | 37 + .../testdata/maven/invalid-syntax.xml | 13 + .../java/pomxmlnet/testdata/maven/not-pom.txt | 1 + .../pomxmlnet/testdata/maven/one-package.xml | 17 + .../pomxmlnet/testdata/maven/parent/pom.xml | 21 + .../pomxmlnet/testdata/maven/transitive.xml | 33 + .../pomxmlnet/testdata/maven/two-packages.xml | 22 + .../maven/with-dependency-management.xml | 37 + .../pomxmlnet/testdata/maven/with-parent.xml | 54 + .../pomxmlnet/testdata/maven/with-scope.xml | 14 + .../testdata/universe/basic-universe.yaml | 60 + .../javascript/nodemodules/extractor.go | 57 + .../language/osv/osvscannerjson/extractor.go | 84 ++ .../osv/osvscannerjson/extractor_test.go | 139 ++ .../language/osv/osvscannerjson/metadata.go | 9 + .../osv/osvscannerjson/testdata/empty.json | 3 + .../multiple-packages-with-vulns.json | 504 +++++++ .../osv/osvscannerjson/testdata/not-json.txt | 1 + .../testdata/one-package-commit.json | 19 + .../osvscannerjson/testdata/one-package.json | 21 + internal/lockfilescalibr/translation.go | 188 +++ internal/lockfilescalibr/translation_test.go | 23 + .../fixtures/santatracker/osv-scanner.toml | 187 --- .../fixtures/zeppelin-server/osv-scanner.toml | 139 -- pkg/config/config.go | 270 ---- pkg/config/config_internal_test.go | 1320 ----------------- .../testdatainner/innerFolder/test.yaml | 0 .../testdatainner/osv-scanner-load-path.toml | 1 - .../fixtures/testdatainner/osv-scanner.toml | 25 - .../fixtures/testdatainner/some-manifest.yaml | 0 pkg/config/fixtures/unknown-key-1.toml | 4 - pkg/config/fixtures/unknown-key-2.toml | 4 - pkg/config/fixtures/unknown-key-3.toml | 4 - pkg/config/fixtures/unknown-key-4.toml | 4 - pkg/config/fixtures/unknown-key-5.toml | 3 - pkg/config/fixtures/unknown-key-6.toml | 1 - pkg/config/fixtures/unknown-key-7.toml | 5 - pkg/depsdev/license.go | 125 -- pkg/grouper/grouper.go | 76 - pkg/grouper/grouper_models.go | 36 - pkg/grouper/grouper_test.go | 155 -- pkg/osvscanner/osvscanner.go | 277 ++-- pkg/spdx/gen.go | 62 - pkg/spdx/licenses.go | 679 --------- pkg/spdx/verify.go | 19 - pkg/spdx/verify_test.go | 37 - 83 files changed, 3141 insertions(+), 3602 deletions(-) rename internal/image/fixtures/{alpine-3.19-alpine-release => alpine-3.18-alpine-release} (100%) create mode 100644 internal/image/fixtures/alpine-3.18-os-release create mode 100644 internal/image/pathtree/pathtree.go create mode 100644 internal/image/pathtree/pathtree_test.go create mode 100644 internal/lockfilescalibr/errors.go create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/extractor.go create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/extractor_test.go create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/empty.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/interpolation.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/invalid-syntax.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/not-pom.txt create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/one-package.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/parent/pom.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/transitive.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/two-packages.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-dependency-management.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-parent.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-scope.xml create mode 100644 internal/lockfilescalibr/language/java/pomxmlnet/testdata/universe/basic-universe.yaml create mode 100644 internal/lockfilescalibr/language/javascript/nodemodules/extractor.go create mode 100644 internal/lockfilescalibr/language/osv/osvscannerjson/extractor.go create mode 100644 internal/lockfilescalibr/language/osv/osvscannerjson/extractor_test.go create mode 100644 internal/lockfilescalibr/language/osv/osvscannerjson/metadata.go create mode 100644 internal/lockfilescalibr/language/osv/osvscannerjson/testdata/empty.json create mode 100644 internal/lockfilescalibr/language/osv/osvscannerjson/testdata/multiple-packages-with-vulns.json create mode 100644 internal/lockfilescalibr/language/osv/osvscannerjson/testdata/not-json.txt create mode 100644 internal/lockfilescalibr/language/osv/osvscannerjson/testdata/one-package-commit.json create mode 100644 internal/lockfilescalibr/language/osv/osvscannerjson/testdata/one-package.json create mode 100644 internal/lockfilescalibr/translation.go create mode 100644 internal/lockfilescalibr/translation_test.go delete mode 100644 pkg/config/config.go delete mode 100644 pkg/config/config_internal_test.go delete mode 100644 pkg/config/fixtures/testdatainner/innerFolder/test.yaml delete mode 100644 pkg/config/fixtures/testdatainner/osv-scanner-load-path.toml delete mode 100644 pkg/config/fixtures/testdatainner/osv-scanner.toml delete mode 100644 pkg/config/fixtures/testdatainner/some-manifest.yaml delete mode 100644 pkg/config/fixtures/unknown-key-1.toml delete mode 100644 pkg/config/fixtures/unknown-key-2.toml delete mode 100644 pkg/config/fixtures/unknown-key-3.toml delete mode 100644 pkg/config/fixtures/unknown-key-4.toml delete mode 100644 pkg/config/fixtures/unknown-key-5.toml delete mode 100644 pkg/config/fixtures/unknown-key-6.toml delete mode 100644 pkg/config/fixtures/unknown-key-7.toml delete mode 100644 pkg/depsdev/license.go delete mode 100644 pkg/grouper/grouper.go delete mode 100644 pkg/grouper/grouper_models.go delete mode 100644 pkg/grouper/grouper_test.go delete mode 100644 pkg/spdx/gen.go delete mode 100644 pkg/spdx/licenses.go delete mode 100644 pkg/spdx/verify.go delete mode 100644 pkg/spdx/verify_test.go diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index fad10f0662..0588c737b0 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -16,10 +16,10 @@ name: Checks on: push: - branches: [main, v2] + branches: [main, v1] pull_request: # The branches below must be a subset of the branches above - branches: [main, v2] + branches: [main, v1] concurrency: # Pushing new changes to a branch will cancel any in-progress CI runs diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9fec7b9e2d..c12837d818 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,10 +13,10 @@ name: "CodeQL" on: push: - branches: [main, v2] + branches: [main, v1] pull_request: # The branches below must be a subset of the branches above - branches: [main, v2] + branches: [main, v1] # Restrict jobs in this workflow to have no permissions by default; permissions # should be granted per job as needed using a dedicated `permissions` block diff --git a/.github/workflows/osv-scanner-unified-action.yml b/.github/workflows/osv-scanner-unified-action.yml index 140e6feabb..fbd2c77f24 100644 --- a/.github/workflows/osv-scanner-unified-action.yml +++ b/.github/workflows/osv-scanner-unified-action.yml @@ -16,11 +16,11 @@ name: OSV-Scanner Scheduled Scan on: pull_request: - branches: ["main", "v2"] + branches: ["main", "v1"] schedule: - cron: "12 12 * * 1" push: - branches: ["main", "v2"] + branches: ["main", "v1"] # Restrict jobs in this workflow to have no permissions by default; permissions # should be granted per job as needed using a dedicated `permissions` block diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 5514c606e1..01706bad3a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -12,7 +12,7 @@ on: schedule: - cron: "32 22 * * 6" push: - branches: ["main", "v2"] + branches: ["main"] # Restrict jobs in this workflow to have no permissions by default; permissions # should be granted per job as needed using a dedicated `permissions` block diff --git a/.prettierignore b/.prettierignore index d63dc2c013..70ca255b4c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ **/fixtures/** +**/testdata/** **/fixtures-go/** /docs/vendor/** /internal/output/html/*template.html diff --git a/cmd/osv-scanner/__snapshots__/main_test.snap b/cmd/osv-scanner/__snapshots__/main_test.snap index 4b3410a1ca..c2219f941d 100755 --- a/cmd/osv-scanner/__snapshots__/main_test.snap +++ b/cmd/osv-scanner/__snapshots__/main_test.snap @@ -349,9 +349,9 @@ overriding license for package Packagist/league/flysystem/1.0.8 with 0BSD | LICENSE VIOLATION | ECOSYSTEM | PACKAGE | VERSION | SOURCE | +-------------------+-----------+------------------------------------------------+---------+-------------------------------------------------------+ | 0BSD | Packagist | league/flysystem | 1.0.8 | fixtures/locks-insecure/composer.lock | -| UNKNOWN | | https://github.com/flutter/buildroot.git | | fixtures/locks-insecure/osv-scanner-flutter-deps.json | -| UNKNOWN | | https://github.com/brendan-duncan/archive.git | | fixtures/locks-insecure/osv-scanner-flutter-deps.json | | UNKNOWN | | https://chromium.googlesource.com/chromium/src | | fixtures/locks-insecure/osv-scanner-flutter-deps.json | +| UNKNOWN | | https://github.com/brendan-duncan/archive.git | | fixtures/locks-insecure/osv-scanner-flutter-deps.json | +| UNKNOWN | | https://github.com/flutter/buildroot.git | | fixtures/locks-insecure/osv-scanner-flutter-deps.json | | UNKNOWN | RubyGems | ast | 2.4.2 | fixtures/locks-many/Gemfile.lock | | 0BSD | Packagist | sentry/sdk | 2.0.4 | fixtures/locks-many/composer.lock | +-------------------+-----------+------------------------------------------------+---------+-------------------------------------------------------+ @@ -926,6 +926,68 @@ Scanned <rootdir>/fixtures/call-analysis-go-project/go.mod file and found 4 pack --- +[TestRun_Docker/Fake_alpine_image - 1] +Pulling docker image ("alpine:non-existent-tag")... + +--- + +[TestRun_Docker/Fake_alpine_image - 2] +Docker command exited with code ("/usr/bin/docker pull -q alpine:non-existent-tag"): 1 +STDERR: +> Error response from daemon: manifest for alpine:non-existent-tag not found: manifest unknown: manifest unknown +failed to run docker command + +--- + +[TestRun_Docker/Fake_image_entirely - 1] +Pulling docker image ("this-image-definitely-does-not-exist-abcde")... + +--- + +[TestRun_Docker/Fake_image_entirely - 2] +Docker command exited with code ("/usr/bin/docker pull -q this-image-definitely-does-not-exist-abcde"): 1 +STDERR: +> Error response from daemon: pull access denied for this-image-definitely-does-not-exist-abcde, repository does not exist or may require 'docker login': denied: requested access to the resource is denied +failed to run docker command + +--- + +[TestRun_Docker/Real_Alpine_image - 1] +Pulling docker image ("alpine:3.18.9")... +Saving docker image ("alpine:3.18.9") to temporary file... +Scanning image... +No issues found + +--- + +[TestRun_Docker/Real_Alpine_image - 2] + +--- + +[TestRun_Docker/Real_empty_image - 1] +Pulling docker image ("hello-world")... +Saving docker image ("hello-world") to temporary file... +Scanning image... + +--- + +[TestRun_Docker/Real_empty_image - 2] +No package sources found, --help for usage information. + +--- + +[TestRun_Docker/Real_empty_image_with_tag - 1] +Pulling docker image ("hello-world:linux")... +Saving docker image ("hello-world:linux") to temporary file... +Scanning image... + +--- + +[TestRun_Docker/Real_empty_image_with_tag - 2] +No package sources found, --help for usage information. + +--- + [TestRun_GithubActions/scanning_osv-scanner_custom_format - 1] Scanned <rootdir>/fixtures/locks-insecure/osv-scanner-flutter-deps.json file as a osv-scanner and found 3 packages +--------------------------------+------+-----------+----------------------------+----------------------------+-------------------------------------------------------+ @@ -2393,7 +2455,7 @@ No issues found --- [TestRun_LockfileWithExplicitParseAs/empty_works_as_an_escape_(no_fixture_because_it's_not_valid_on_Windows) - 2] -open <rootdir>/path/to/my:file: no such file or directory +stat <rootdir>/path/to/my:file: no such file or directory --- @@ -2402,7 +2464,7 @@ open <rootdir>/path/to/my:file: no such file or directory --- [TestRun_LockfileWithExplicitParseAs/empty_works_as_an_escape_(no_fixture_because_it's_not_valid_on_Windows)#01 - 2] -open <rootdir>/path/to/my:project/package-lock.json: no such file or directory +stat <rootdir>/path/to/my:project/package-lock.json: no such file or directory --- @@ -2411,7 +2473,7 @@ open <rootdir>/path/to/my:project/package-lock.json: no such file or directory --- [TestRun_LockfileWithExplicitParseAs/files_that_error_on_parsing_stop_parsable_files_from_being_checked - 2] -(extracting as Cargo.lock) could not extract from <rootdir>/fixtures/locks-insecure/my-package-lock.json: toml: line 1: expected '.' or '=', but got '{' instead +(extracting as rust/Cargolock) could not extract from <rootdir>/fixtures/locks-insecure/my-package-lock.json: toml: line 1: expected '.' or '=', but got '{' instead --- @@ -2469,7 +2531,7 @@ No issues found --- [TestRun_LockfileWithExplicitParseAs/parse-as_takes_priority,_even_if_it's_wrong - 2] -(extracting as package-lock.json) could not extract from <rootdir>/fixtures/locks-many/yarn.lock: invalid character '#' looking for beginning of value +(extracting as javascript/packagelockjson) could not extract from "<rootdir>/fixtures/locks-many/yarn.lock": invalid character '#' looking for beginning of value --- @@ -2522,7 +2584,7 @@ No issues found --- -[TestRun_MavenTransitive/resolve_transitive_dependencies_with_native_datda_source - 1] +[TestRun_MavenTransitive/resolve_transitive_dependencies_with_native_data_source - 1] Scanned <rootdir>/fixtures/maven-transitive/registry.xml file as a pom.xml and found 59 packages +-------------------------------------+------+-----------+-----------------------------------------------+---------+----------------------------------------+ | OSV URL | CVSS | ECOSYSTEM | PACKAGE | VERSION | SOURCE | @@ -2536,7 +2598,7 @@ Scanned <rootdir>/fixtures/maven-transitive/registry.xml file as a pom.xml and f --- -[TestRun_MavenTransitive/resolve_transitive_dependencies_with_native_datda_source - 2] +[TestRun_MavenTransitive/resolve_transitive_dependencies_with_native_data_source - 2] --- @@ -2673,7 +2735,7 @@ Total 3 packages affected by 6 vulnerabilities (2 Critical, 0 High, 4 Medium, 0 npm +--------------------------------------------------------------+ | Source:docker:../../internal/image/fixtures/test-node_module | -| s-npm-full.tar:/usr/app/node_modules/.package-lock.json | +| s-npm-full.tar:/prod/app/node_modules/.package-lock.json | +----------+-------------------+------------------+------------+ | PACKAGE | INSTALLED VERSION | FIX AVAILABLE | VULN COUNT | +----------+-------------------+------------------+------------+ diff --git a/cmd/osv-scanner/fixtures/locks-requirements/my-requirements.txt b/cmd/osv-scanner/fixtures/locks-requirements/my-requirements.txt index 7e1060246f..0e463a4d02 100644 --- a/cmd/osv-scanner/fixtures/locks-requirements/my-requirements.txt +++ b/cmd/osv-scanner/fixtures/locks-requirements/my-requirements.txt @@ -1 +1 @@ -flask +flask==1.0.0 diff --git a/cmd/osv-scanner/fixtures/locks-requirements/requirements-dev.txt b/cmd/osv-scanner/fixtures/locks-requirements/requirements-dev.txt index 7e66a17d49..4fae28300e 100644 --- a/cmd/osv-scanner/fixtures/locks-requirements/requirements-dev.txt +++ b/cmd/osv-scanner/fixtures/locks-requirements/requirements-dev.txt @@ -1 +1 @@ -black +black==1.0.0 diff --git a/cmd/osv-scanner/fixtures/locks-requirements/requirements.txt b/cmd/osv-scanner/fixtures/locks-requirements/requirements.txt index d0dae5a60f..911f55bcf9 100644 --- a/cmd/osv-scanner/fixtures/locks-requirements/requirements.txt +++ b/cmd/osv-scanner/fixtures/locks-requirements/requirements.txt @@ -1,3 +1,3 @@ -flask -flask-cors +flask==1.0.0 +flask-cors==1.0.0 pandas==0.23.4 diff --git a/cmd/osv-scanner/fixtures/locks-requirements/the_requirements_for_test.txt b/cmd/osv-scanner/fixtures/locks-requirements/the_requirements_for_test.txt index e079f8a603..35663c020e 100644 --- a/cmd/osv-scanner/fixtures/locks-requirements/the_requirements_for_test.txt +++ b/cmd/osv-scanner/fixtures/locks-requirements/the_requirements_for_test.txt @@ -1 +1 @@ -pytest +pytest==1.0.0 diff --git a/cmd/osv-scanner/fixtures/sbom-insecure/osv-scanner.toml b/cmd/osv-scanner/fixtures/sbom-insecure/osv-scanner.toml index 80e5b8b2ca..4a3e9070b8 100644 --- a/cmd/osv-scanner/fixtures/sbom-insecure/osv-scanner.toml +++ b/cmd/osv-scanner/fixtures/sbom-insecure/osv-scanner.toml @@ -1,64 +1,3 @@ -[[IgnoredVulns]] -id = "GO-2022-0274" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "GO-2022-0493" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "GHSA-vpvm-3wq2-2wvm" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "GHSA-m8cg-xc2p-r3fc" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "GHSA-g2j6-57v7-gm8c" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "GHSA-f3fp-gc8g-vw66" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "DLA-3008-1" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "DLA-3012-1" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "DLA-3022-1" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "DLA-3051-1" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "CVE-2022-37434" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "CVE-2018-25032" -# ignoreUntil = n/a -reason = "This is an intentionally vulnerable test sbom" - -[[IgnoredVulns]] -id = "GHSA-xr7r-f8xq-vfvv" -# ignoreUntil = n/a +[[PackageOverrides]] +ignore = true reason = "This is an intentionally vulnerable test sbom" diff --git a/cmd/osv-scanner/main.go b/cmd/osv-scanner/main.go index 595b1afe49..3c891f338c 100644 --- a/cmd/osv-scanner/main.go +++ b/cmd/osv-scanner/main.go @@ -47,6 +47,18 @@ func run(args []string, stdout, stderr io.Writer) int { }, } + // If ExitErrHandler is not set, cli will use the default cli.HandleExitCoder. + // This is not ideal as cli.HandleExitCoder checks if the error implements cli.ExitCode interface. + // + // 99% of the time, this is fine, as we do not implement cli.ExitCode in our errors, so errors pass through + // that handler untouched. + // However, because of Go's duck typing, any error that happens to have a ExitCode() function + // (e.g. *exec.ExitError) will be assumed to implement cli.ExitCode interface and cause the program to exit + // early without proper error handling. + // + // This removes the handler entirely so that behavior will not unexpectedly happen. + app.ExitErrHandler = func(_ *cli.Context, _ error) {} + args = insertDefaultCommand(args, app.Commands, app.DefaultCommand, stdout, stderr) if err := app.Run(args); err != nil { diff --git a/cmd/osv-scanner/main_test.go b/cmd/osv-scanner/main_test.go index 1e25da3c03..a1ea382a99 100644 --- a/cmd/osv-scanner/main_test.go +++ b/cmd/osv-scanner/main_test.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "reflect" + "runtime" "strings" "testing" @@ -523,7 +524,12 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - testCli(t, tt) + stdout, stderr := runCli(t, tt) + + testutility.NewSnapshot().MatchText(t, stdout) + testutility.NewSnapshot().WithWindowsReplacements(map[string]string{ + "CreateFile": "stat", + }).MatchText(t, stderr) }) } } @@ -744,6 +750,51 @@ func TestRun_Licenses(t *testing.T) { } } +func TestRun_Docker(t *testing.T) { + t.Parallel() + + testutility.SkipIfNotAcceptanceTesting(t, "Takes a long time to pull down images") + + tests := []cliTestCase{ + { + name: "Fake alpine image", + args: []string{"", "--docker", "alpine:non-existent-tag"}, + exit: 127, + }, + { + name: "Fake image entirely", + args: []string{"", "--docker", "this-image-definitely-does-not-exist-abcde"}, + exit: 127, + }, + // TODO: How to prevent these snapshots from changing constantly + { + name: "Real empty image", + args: []string{"", "--docker", "hello-world"}, + exit: 128, // No packages found + }, + { + name: "Real empty image with tag", + args: []string{"", "--docker", "hello-world:linux"}, + exit: 128, // No package found + }, + { + name: "Real Alpine image", + args: []string{"", "--docker", "alpine:3.18.9"}, + exit: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Only test on linux, and mac/windows CI/CD does not come with docker preinstalled + if runtime.GOOS == "linux" { + testCli(t, tt) + } + }) + } +} + func TestRun_OCIImage(t *testing.T) { t.Parallel() @@ -938,6 +989,11 @@ func TestRun_MavenTransitive(t *testing.T) { args: []string{"", "--config=./fixtures/osv-scanner-empty-config.toml", "-L", "pom.xml:./fixtures/maven-transitive/registry.xml"}, exit: 1, }, + { + name: "resolve transitive dependencies with native data source", + args: []string{"", "--config=./fixtures/osv-scanner-empty-config.toml", "--experimental-resolution-data-source=native", "-L", "pom.xml:./fixtures/maven-transitive/registry.xml"}, + exit: 1, + }, } for _, tt := range tests { diff --git a/cmd/osv-scanner/scan/main.go b/cmd/osv-scanner/scan/main.go index ffa5281205..bf763f3b15 100644 --- a/cmd/osv-scanner/scan/main.go +++ b/cmd/osv-scanner/scan/main.go @@ -4,9 +4,14 @@ import ( "errors" "fmt" "io" + "net/http" "os" + "os/exec" + "path/filepath" + "runtime" "slices" "strings" + "time" "github.com/google/osv-scanner/internal/spdx" "github.com/google/osv-scanner/pkg/osvscanner" @@ -31,10 +36,10 @@ func Command(stdout, stderr io.Writer, r *reporter.Reporter) *cli.Command { Usage: "scans various mediums for dependencies and matches it against the OSV database", Description: "scans various mediums for dependencies and matches it against the OSV database", Flags: []cli.Flag{ - &cli.StringSliceFlag{ + &cli.StringFlag{ Name: "docker", Aliases: []string{"D"}, - Usage: "scan docker image with this name. Warning: Only run this on a trusted container image, as it runs the container image to retrieve the package versions", + Usage: "scan docker image with this name. This is a convenience function which runs `docker save` before scanning the saved image using --oci-image", TakesFile: false, }, &cli.StringSliceFlag{ @@ -73,6 +78,10 @@ func Command(stdout, stderr io.Writer, r *reporter.Reporter) *cli.Command { return fmt.Errorf("unsupported output format \"%s\" - must be one of: %s", s, strings.Join(reporter.Format(), ", ")) }, }, + &cli.BoolFlag{ + Name: "serve", + Usage: "output as HTML result and serve it locally", + }, &cli.BoolFlag{ Name: "json", Usage: "sets output to json (deprecated, use --format json instead)", @@ -204,6 +213,22 @@ func action(context *cli.Context, stdout, stderr io.Writer) (reporter.Reporter, } outputPath := context.String("output") + serve := context.Bool("serve") + if serve { + format = "html" + if outputPath == "" { + // Create a temporary directory + tmpDir, err := os.MkdirTemp("", "osv-scanner-result") + if err != nil { + return nil, fmt.Errorf("failed creating temporary directory: %w\n"+ + "Please use `--output result.html` to specify the output path", err) + } + + // Remove the created temporary directory after + defer os.RemoveAll(tmpDir) + outputPath = filepath.Join(tmpDir, "index.html") + } + } termWidth := 0 var err error @@ -258,15 +283,15 @@ func action(context *cli.Context, stdout, stderr io.Writer) (reporter.Reporter, } vulnResult, err := osvscanner.DoScan(osvscanner.ScannerActions{ - LockfilePaths: context.StringSlice("lockfile"), - SBOMPaths: context.StringSlice("sbom"), - DockerContainerNames: context.StringSlice("docker"), - Recursive: context.Bool("recursive"), - SkipGit: context.Bool("skip-git"), - NoIgnore: context.Bool("no-ignore"), - ConfigOverridePath: context.String("config"), - DirectoryPaths: context.Args().Slice(), - CallAnalysisStates: callAnalysisStates, + LockfilePaths: context.StringSlice("lockfile"), + SBOMPaths: context.StringSlice("sbom"), + DockerImageName: context.String("docker"), + Recursive: context.Bool("recursive"), + SkipGit: context.Bool("skip-git"), + NoIgnore: context.Bool("no-ignore"), + ConfigOverridePath: context.String("config"), + DirectoryPaths: context.Args().Slice(), + CallAnalysisStates: callAnalysisStates, ExperimentalScannerActions: osvscanner.ExperimentalScannerActions{ LocalDBPath: context.String("experimental-local-db-path"), DownloadDatabases: context.Bool("experimental-download-offline-databases"), @@ -296,6 +321,55 @@ func action(context *cli.Context, stdout, stderr io.Writer) (reporter.Reporter, return r, fmt.Errorf("failed to write output: %w", errPrint) } + // Auto-open outputted HTML file for users. + if outputPath != "" { + if serve { + serveHTML(r, outputPath) + } else if format == "html" { + openHTML(r, outputPath) + } + } + // This may be nil. return r, err } + +// openHTML opens the outputted HTML file. +func openHTML(r reporter.Reporter, outputPath string) { + // Open the outputted HTML file in the default browser. + r.Infof("Opening %s...\n", outputPath) + var err error + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", outputPath).Start() + case "windows": + err = exec.Command("start", "", outputPath).Start() + case "darwin": // macOS + err = exec.Command("open", outputPath).Start() + default: + r.Infof("Unsupported OS.\n") + } + + if err != nil { + r.Errorf("Failed to open: %s.\n Please manually open the outputted HTML file: %s\n", err, outputPath) + } +} + +// Serve the single HTML file for remote accessing. +// The program will keep running to serve the HTML report on localhost +// until the user manually terminates it (e.g. using Ctrl+C). +func serveHTML(r reporter.Reporter, outputPath string) { + servePort := "8000" + localhostURL := fmt.Sprintf("http://localhost:%s/", servePort) + r.Infof("Serving HTML report at %s.\nIf you are accessing remotely, use the following SSH command:\n`ssh -L local_port:destination_server_ip:%s ssh_server_hostname`\n", localhostURL, servePort) + server := &http.Server{ + Addr: ":" + servePort, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, outputPath) + }), + ReadHeaderTimeout: 3 * time.Second, + } + if err := server.ListenAndServe(); err != nil { + r.Errorf("Failed to start server: %v\n", err) + } +} diff --git a/go.mod b/go.mod index c8998169e6..190c3fc96e 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,24 @@ module github.com/google/osv-scanner go 1.22.7 require ( - deps.dev/api/v3 v3.0.0-20241114233204-66e2aed8456e - deps.dev/util/maven v0.0.0-20241114233204-66e2aed8456e - deps.dev/util/resolve v0.0.0-20241114233204-66e2aed8456e - deps.dev/util/semver v0.0.0-20241114233204-66e2aed8456e + deps.dev/api/v3 v3.0.0-20241010035105-b3ba03369df1 + deps.dev/util/maven v0.0.0-20241010035105-b3ba03369df1 + deps.dev/util/resolve v0.0.0-20241010035105-b3ba03369df1 + deps.dev/util/semver v0.0.0-20241010035105-b3ba03369df1 github.com/BurntSushi/toml v1.4.0 github.com/CycloneDX/cyclonedx-go v0.9.1 github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.2.2 + github.com/charmbracelet/bubbletea v1.1.1 github.com/charmbracelet/glamour v0.8.0 - github.com/charmbracelet/lipgloss v1.0.0 - github.com/dghubble/trie v0.1.0 + github.com/charmbracelet/lipgloss v0.13.0 github.com/gkampitakis/go-snaps v0.5.7 - github.com/go-git/go-billy/v5 v5.6.0 + github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 github.com/google/go-cmp v0.6.0 github.com/google/go-containerregistry v0.20.2 + github.com/google/osv-scalibr v0.1.4-0.20241031120023-761ca671aacb github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd - github.com/jedib0t/go-pretty/v6 v6.6.2 + github.com/jedib0t/go-pretty/v6 v6.6.0 github.com/muesli/reflow v0.3.0 github.com/owenrumney/go-sarif/v2 v2.3.3 github.com/package-url/packageurl-go v0.1.3 @@ -30,36 +30,34 @@ require ( github.com/tidwall/pretty v1.2.1 github.com/tidwall/sjson v1.2.5 github.com/urfave/cli/v2 v2.27.5 - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f - golang.org/x/mod v0.22.0 - golang.org/x/net v0.31.0 - golang.org/x/sync v0.9.0 - golang.org/x/term v0.26.0 + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c + golang.org/x/mod v0.21.0 + golang.org/x/net v0.30.0 + golang.org/x/sync v0.8.0 + golang.org/x/term v0.25.0 golang.org/x/vuln v1.0.4 - google.golang.org/grpc v1.68.0 - google.golang.org/protobuf v1.35.2 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 ) require ( dario.cat/mergo v1.0.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.4.5 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/cyphar/filepath-securejoin v0.2.5 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gkampitakis/ciinfo v0.3.0 // indirect @@ -82,14 +80,13 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc3 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/spdx/gordf v0.0.0-20221230105357-b735bd5aac89 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -99,10 +96,10 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yuin/goldmark v1.7.4 // indirect github.com/yuin/goldmark-emoji v1.0.3 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index d293984a1e..36bfaa2ada 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,20 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -deps.dev/api/v3 v3.0.0-20241114233204-66e2aed8456e h1:BMnLPyddIsU4t+dWdyCzuGLPyX2Z2NlZiPERck26504= -deps.dev/api/v3 v3.0.0-20241114233204-66e2aed8456e/go.mod h1:DyBY3wNVqRCwvb4tLvz6LL/FupH3FMflEROyQAv2Vi0= -deps.dev/util/maven v0.0.0-20241114233204-66e2aed8456e h1:reRzBTKgHdQX8nLxuJVB0OEiwrJMxuwJ7b9Ryeug7NQ= -deps.dev/util/maven v0.0.0-20241114233204-66e2aed8456e/go.mod h1:SBW3EribdkZYk6zxi5oVn/ZECvi4ixb7EGgEWfSimNk= -deps.dev/util/resolve v0.0.0-20241114233204-66e2aed8456e h1:EuDbMM7J7T/8M+dlTZa4qzB/BBIRh1naqhnwKj893Ek= -deps.dev/util/resolve v0.0.0-20241114233204-66e2aed8456e/go.mod h1:XXi6yRYqhtxw5DvGX/mbG6fHSLn8OgoPowNd8EAxDgk= -deps.dev/util/semver v0.0.0-20241114233204-66e2aed8456e h1:aKkV/WSPvyJRwhVGv4kxaOZFUFdpDXvVse1ItUZyOjw= -deps.dev/util/semver v0.0.0-20241114233204-66e2aed8456e/go.mod h1:jkcH+k02gWHBiZ7G4OnUOkSZ6WDq54Pt5DrOA8FN8Uo= +deps.dev/api/v3 v3.0.0-20241010035105-b3ba03369df1 h1:qvrLinmQrkOLmguTE9FpRfC/e2iud/eVMWigXXTdrdA= +deps.dev/api/v3 v3.0.0-20241010035105-b3ba03369df1/go.mod h1:DyBY3wNVqRCwvb4tLvz6LL/FupH3FMflEROyQAv2Vi0= +deps.dev/util/maven v0.0.0-20241010035105-b3ba03369df1 h1:PWgfyz6h15n4dbcSAL/3lSiXl8foQZCzUWUqEjNLNvI= +deps.dev/util/maven v0.0.0-20241010035105-b3ba03369df1/go.mod h1:SBW3EribdkZYk6zxi5oVn/ZECvi4ixb7EGgEWfSimNk= +deps.dev/util/resolve v0.0.0-20241010035105-b3ba03369df1 h1:nHefSxxfjdmo+zn/8fEcfSUkTXi+LKnBNvul21ZI9qw= +deps.dev/util/resolve v0.0.0-20241010035105-b3ba03369df1/go.mod h1:XXi6yRYqhtxw5DvGX/mbG6fHSLn8OgoPowNd8EAxDgk= +deps.dev/util/semver v0.0.0-20241010035105-b3ba03369df1 h1:t4P0dCCNIrV84B5d7kOIAzji+HrO303Nrw9BB4ktBy0= +deps.dev/util/semver v0.0.0-20241010035105-b3ba03369df1/go.mod h1:jkcH+k02gWHBiZ7G4OnUOkSZ6WDq54Pt5DrOA8FN8Uo= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CycloneDX/cyclonedx-go v0.9.1 h1:yffaWOZsv77oTJa/SdVZYdgAgFioCeycBUKkqS2qzQM= github.com/CycloneDX/cyclonedx-go v0.9.1/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKcTwu8u0ccsACEsw= 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= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= @@ -44,18 +44,18 @@ github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1l github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= -github.com/charmbracelet/bubbletea v1.2.2 h1:EMz//Ky/aFS2uLcKqpCst5UOE6z5CFDGRsUpyXz0chs= -github.com/charmbracelet/bubbletea v1.2.2/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= +github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= -github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= -github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= -github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= -github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= @@ -64,13 +64,11 @@ github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9N github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= -github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dghubble/trie v0.1.0 h1:kJnjBLFFElBwS60N4tkPvnLhnpcDxbBjIulgI8CpNGM= -github.com/dghubble/trie v0.1.0/go.mod h1:sOmnzfBNH7H92ow2292dDFWNsVQuh/izuD7otCYb1ak= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= @@ -95,8 +93,8 @@ github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +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.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= @@ -105,14 +103,14 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l 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= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= +github.com/google/osv-scalibr v0.1.4-0.20241031120023-761ca671aacb h1:A7IvUJk8r3wMuuAMWxwbkE3WBp+oF/v7CcEt3nCy+lI= +github.com/google/osv-scalibr v0.1.4-0.20241031120023-761ca671aacb/go.mod h1:MbEYB+PKqEGjwMdpcoO5DWpi0+57jYgYcw2jlRy8O9Q= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= @@ -121,8 +119,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd h1:EVX1s+X github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.6.2 h1:27bLj3nRODzaiA7tPIxy9UVWHoPspFfME9XxgwiiNsM= -github.com/jedib0t/go-pretty/v6 v6.6.2/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jedib0t/go-pretty/v6 v6.6.0 h1:wmZVuAcEkZRT+Aq1xXpE8IGat4vE5WXOMmBpbQqERXw= +github.com/jedib0t/go-pretty/v6 v6.6.0/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= @@ -159,12 +157,12 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= -github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U= github.com/owenrumney/go-sarif/v2 v2.3.3 h1:ubWDJcF5i3L/EIOER+ZyQ03IfplbSU1BLOE26uKQIIU= github.com/owenrumney/go-sarif/v2 v2.3.3/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= @@ -254,14 +252,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -271,13 +269,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -292,15 +290,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -310,14 +308,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= -golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I= golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -326,10 +324,10 @@ google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1: google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= -google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/image/__snapshots__/image_test.snap b/internal/image/__snapshots__/image_test.snap index 9d957ad396..58b8b54897 100755 --- a/internal/image/__snapshots__/image_test.snap +++ b/internal/image/__snapshots__/image_test.snap @@ -4,7 +4,7 @@ "Lockfiles": [ { "filePath": "/lib/apk/db/installed", - "parsedAs": "apk-installed", + "parsedAs": "os/apk", "packages": [ { "name": "alpine-baselayout", @@ -186,7 +186,7 @@ "Lockfiles": [ { "filePath": "/go/bin/more-vuln-overwrite-less-vuln", - "parsedAs": "go-binary", + "parsedAs": "go/binary", "packages": [ { "name": "github.com/BurntSushi/toml", @@ -214,7 +214,7 @@ }, { "filePath": "/go/bin/ptf-1.2.0", - "parsedAs": "go-binary", + "parsedAs": "go/binary", "packages": [ { "name": "github.com/BurntSushi/toml", @@ -242,7 +242,7 @@ }, { "filePath": "/go/bin/ptf-1.3.0", - "parsedAs": "go-binary", + "parsedAs": "go/binary", "packages": [ { "name": "github.com/BurntSushi/toml", @@ -270,7 +270,7 @@ }, { "filePath": "/go/bin/ptf-1.3.0-moved", - "parsedAs": "go-binary", + "parsedAs": "go/binary", "packages": [ { "name": "github.com/BurntSushi/toml", @@ -298,7 +298,7 @@ }, { "filePath": "/go/bin/ptf-1.4.0", - "parsedAs": "go-binary", + "parsedAs": "go/binary", "packages": [ { "name": "github.com/BurntSushi/toml", @@ -326,7 +326,7 @@ }, { "filePath": "/go/bin/ptf-vulnerable", - "parsedAs": "go-binary", + "parsedAs": "go/binary", "packages": [ { "name": "github.com/BurntSushi/toml", @@ -354,7 +354,7 @@ }, { "filePath": "/lib/apk/db/installed", - "parsedAs": "apk-installed", + "parsedAs": "os/apk", "packages": [ { "name": "alpine-baselayout", @@ -536,7 +536,7 @@ "Lockfiles": [ { "filePath": "/lib/apk/db/installed", - "parsedAs": "apk-installed", + "parsedAs": "os/apk", "packages": [ { "name": "alpine-baselayout", @@ -754,7 +754,7 @@ "Lockfiles": [ { "filePath": "/lib/apk/db/installed", - "parsedAs": "apk-installed", + "parsedAs": "os/apk", "packages": [ { "name": "alpine-baselayout", @@ -963,8 +963,8 @@ ] }, { - "filePath": "/usr/app/node_modules/.package-lock.json", - "parsedAs": "node_modules", + "filePath": "/prod/app/node_modules/.package-lock.json", + "parsedAs": "javascript/nodemodules", "packages": [ { "name": "cryo", @@ -1011,7 +1011,7 @@ "Lockfiles": [ { "filePath": "/lib/apk/db/installed", - "parsedAs": "apk-installed", + "parsedAs": "os/apk", "packages": [ { "name": "alpine-baselayout", @@ -1229,7 +1229,7 @@ "Lockfiles": [ { "filePath": "/lib/apk/db/installed", - "parsedAs": "apk-installed", + "parsedAs": "os/apk", "packages": [ { "name": "alpine-baselayout", @@ -1447,7 +1447,7 @@ "Lockfiles": [ { "filePath": "/lib/apk/db/installed", - "parsedAs": "apk-installed", + "parsedAs": "os/apk", "packages": [ { "name": "alpine-baselayout", @@ -1665,7 +1665,7 @@ "Lockfiles": [ { "filePath": "/lib/apk/db/installed", - "parsedAs": "apk-installed", + "parsedAs": "os/apk", "packages": [ { "name": "alpine-baselayout", diff --git a/internal/image/extractor.go b/internal/image/extractor.go index 6ddb7f9f16..18dad0ed63 100644 --- a/internal/image/extractor.go +++ b/internal/image/extractor.go @@ -1,57 +1,79 @@ package image import ( + "context" "errors" "fmt" - "os" - "path" - "sort" - + "io/fs" + "strings" + + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem" + "github.com/google/osv-scalibr/extractor/filesystem/language/golang/gobinary" + "github.com/google/osv-scalibr/extractor/filesystem/os/apk" + "github.com/google/osv-scalibr/extractor/filesystem/os/dpkg" + "github.com/google/osv-scanner/internal/lockfilescalibr" + "github.com/google/osv-scanner/internal/lockfilescalibr/language/javascript/nodemodules" "github.com/google/osv-scanner/pkg/lockfile" ) // artifactExtractors contains only extractors for artifacts that are important in // the final layer of a container image -var artifactExtractors map[string]lockfile.Extractor = map[string]lockfile.Extractor{ - "node_modules": lockfile.NodeModulesExtractor{}, - "apk-installed": lockfile.ApkInstalledExtractor{}, - "dpkg": lockfile.DpkgStatusExtractor{}, - "go-binary": lockfile.GoBinaryExtractor{}, -} - -type extractorPair struct { - extractor lockfile.Extractor - name string +var artifactExtractors []filesystem.Extractor = []filesystem.Extractor{ + // TODO: Using nodemodules extractor to minimize changes of snapshots + // After annotations are added, we should switch to using packagejson. + // packagejson.New(packagejson.DefaultConfig()), + nodemodules.Extractor{}, + + apk.New(apk.DefaultConfig()), + gobinary.New(gobinary.DefaultConfig()), + // TODO: Add tests for debian containers + dpkg.New(dpkg.DefaultConfig()), } -func findArtifactExtractor(path string) []extractorPair { +func findArtifactExtractor(path string, fileInfo fs.FileInfo) []filesystem.Extractor { // Use ShouldExtract to collect and return a slice of artifactExtractors - var extractors []extractorPair - for name, extractor := range artifactExtractors { - if extractor.ShouldExtract(path) { - extractors = append(extractors, extractorPair{extractor, name}) + var extractors []filesystem.Extractor + for _, extractor := range artifactExtractors { + if extractor.FileRequired(path, fileInfo) { + extractors = append(extractors, extractor) } } return extractors } -func extractArtifactDeps(path string, layer *Layer) (lockfile.Lockfile, error) { - foundExtractors := findArtifactExtractor(path) +// Note: Output is non deterministic +func extractArtifactDeps(extractPath string, layer *Layer) ([]*extractor.Inventory, error) { + pathFileInfo, err := layer.Stat(extractPath) + if err != nil { + return nil, fmt.Errorf("attempted to get FileInfo but failed: %w", err) + } + + scalibrPath := strings.TrimPrefix(extractPath, "/") + foundExtractors := findArtifactExtractor(scalibrPath, pathFileInfo) if len(foundExtractors) == 0 { - return lockfile.Lockfile{}, fmt.Errorf("%w for %s", lockfile.ErrExtractorNotFound, path) + return nil, fmt.Errorf("%w for %s", lockfilescalibr.ErrExtractorNotFound, extractPath) } - packages := []lockfile.PackageDetails{} + inventories := []*extractor.Inventory{} var extractedAs string - for _, extPair := range foundExtractors { + for _, extractor := range foundExtractors { // File has to be reopened per extractor as each extractor moves the read cursor - f, err := OpenLayerFile(path, layer) + f, err := layer.Open(extractPath) if err != nil { - return lockfile.Lockfile{}, fmt.Errorf("attempted to open file but failed: %w", err) + return nil, fmt.Errorf("attempted to open file but failed: %w", err) + } + + scanInput := &filesystem.ScanInput{ + FS: layer, + Path: scalibrPath, + Root: "/", + Reader: f, + Info: pathFileInfo, } - newPackages, err := extPair.extractor.Extract(f) + newPackages, err := extractor.Extract(context.Background(), scanInput) f.Close() if err != nil { @@ -59,76 +81,33 @@ func extractArtifactDeps(path string, layer *Layer) (lockfile.Lockfile, error) { continue } - return lockfile.Lockfile{}, fmt.Errorf("(extracting as %s) %w", extPair.name, err) + return nil, fmt.Errorf("(extracting as %s) %w", extractor.Name(), err) } - extractedAs = extPair.name - packages = newPackages - // TODO(rexpan): Determine if it's acceptable to have multiple extractors + for i := range newPackages { + newPackages[i].Extractor = extractor + } + + extractedAs = extractor.Name() + inventories = newPackages + // TODO(rexpan): Determine if this it's acceptable to have multiple extractors // extract from the same file successfully break } if extractedAs == "" { - return lockfile.Lockfile{}, fmt.Errorf("%w for %s", lockfile.ErrExtractorNotFound, path) + return nil, fmt.Errorf("%w for %s", lockfilescalibr.ErrExtractorNotFound, extractPath) } - // Sort to have deterministic output, and to match behavior of lockfile.extractDeps - sort.Slice(packages, func(i, j int) bool { - if packages[i].Name == packages[j].Name { - return packages[i].Version < packages[j].Version + // Perform any one-off translations here + for _, inv := range inventories { + // Scalibr uses go to indicate go compiler version + // We specifically cares about the stdlib version inside the package + // so convert the package name from go to stdlib + if inv.Ecosystem() == "Go" && inv.Name == "go" { + inv.Name = "stdlib" } - - return packages[i].Name < packages[j].Name - }) - - return lockfile.Lockfile{ - FilePath: path, - ParsedAs: extractedAs, - Packages: packages, - }, nil -} - -// A File represents a file that exists in an image -type File struct { - *os.File - - layer *Layer - path string -} - -func (f File) Open(openPath string) (lockfile.NestedDepFile, error) { - // use path instead of filepath, because container is always in Unix paths (for now) - if path.IsAbs(openPath) { - return OpenLayerFile(openPath, f.layer) - } - - absPath := path.Join(f.path, openPath) - - return OpenLayerFile(absPath, f.layer) -} - -func (f File) Path() string { - return f.path -} - -func OpenLayerFile(path string, layer *Layer) (File, error) { - fileNode, err := layer.getFileNode(path) - if err != nil { - return File{}, err } - file, err := fileNode.Open() - if err != nil { - return File{}, err - } - - return File{ - File: file, - path: path, - layer: layer, - }, nil + return inventories, nil } - -var _ lockfile.DepFile = File{} -var _ lockfile.NestedDepFile = File{} diff --git a/internal/image/fixtures/alpine-3.19-alpine-release b/internal/image/fixtures/alpine-3.18-alpine-release similarity index 100% rename from internal/image/fixtures/alpine-3.19-alpine-release rename to internal/image/fixtures/alpine-3.18-alpine-release diff --git a/internal/image/fixtures/alpine-3.18-os-release b/internal/image/fixtures/alpine-3.18-os-release new file mode 100644 index 0000000000..ffb92a8cd4 --- /dev/null +++ b/internal/image/fixtures/alpine-3.18-os-release @@ -0,0 +1,7 @@ +/ # cat /etc/os-release +NAME="Alpine Linux" +ID=alpine +VERSION_ID=3.18.1 +PRETTY_NAME="Alpine Linux v3.18" +HOME_URL="https://alpinelinux.org/" +BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues" diff --git a/internal/image/fixtures/test-alpine.Dockerfile b/internal/image/fixtures/test-alpine.Dockerfile index 5cf22e2812..d6aa79f1c8 100644 --- a/internal/image/fixtures/test-alpine.Dockerfile +++ b/internal/image/fixtures/test-alpine.Dockerfile @@ -1,4 +1,5 @@ FROM alpine:3.10@sha256:451eee8bedcb2f029756dc3e9d73bab0e7943c1ac55cff3a4861c52a0fdd3e98 -# Switch the version to 3.19 to show the advisories published for the latest alpine versions -COPY "alpine-3.19-alpine-release" "/etc/alpine-release" +# Switch the version to 3.18 to show the advisories published for the latest alpine versions +COPY "alpine-3.18-alpine-release" "/etc/alpine-release" +COPY "alpine-3.18-os-release" "/etc/os-release" diff --git a/internal/image/fixtures/test-node_modules-npm-empty.Dockerfile b/internal/image/fixtures/test-node_modules-npm-empty.Dockerfile index aa559ba285..67ff3b79f7 100644 --- a/internal/image/fixtures/test-node_modules-npm-empty.Dockerfile +++ b/internal/image/fixtures/test-node_modules-npm-empty.Dockerfile @@ -2,7 +2,7 @@ ARG MANAGER_VERSION="10.2.4" FROM node:20-alpine@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c -WORKDIR /usr/app +WORKDIR /prod/app # install the desired package manager RUN npm i -g "npm@$MANAGER_VERSION" diff --git a/internal/image/fixtures/test-node_modules-npm-full.Dockerfile b/internal/image/fixtures/test-node_modules-npm-full.Dockerfile index df412b7a12..96e136b5f7 100644 --- a/internal/image/fixtures/test-node_modules-npm-full.Dockerfile +++ b/internal/image/fixtures/test-node_modules-npm-full.Dockerfile @@ -2,7 +2,7 @@ ARG MANAGER_VERSION="10.2.4" FROM node:20-alpine@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c -WORKDIR /usr/app +WORKDIR /prod/app # install the desired package manager RUN npm i -g "npm@$MANAGER_VERSION" diff --git a/internal/image/fixtures/test-node_modules-pnpm-empty.Dockerfile b/internal/image/fixtures/test-node_modules-pnpm-empty.Dockerfile index 8912eef5d0..7a221ca7ea 100644 --- a/internal/image/fixtures/test-node_modules-pnpm-empty.Dockerfile +++ b/internal/image/fixtures/test-node_modules-pnpm-empty.Dockerfile @@ -2,7 +2,7 @@ ARG MANAGER_VERSION="8.15.4" FROM node:20-alpine@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c -WORKDIR /usr/app +WORKDIR /prod/app # install the desired package manager RUN npm i -g "pnpm@$MANAGER_VERSION" diff --git a/internal/image/fixtures/test-node_modules-pnpm-full.Dockerfile b/internal/image/fixtures/test-node_modules-pnpm-full.Dockerfile index 97a37c652a..80e1ee6519 100644 --- a/internal/image/fixtures/test-node_modules-pnpm-full.Dockerfile +++ b/internal/image/fixtures/test-node_modules-pnpm-full.Dockerfile @@ -2,7 +2,7 @@ ARG MANAGER_VERSION="8.15.4" FROM node:20-alpine@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c -WORKDIR /usr/app +WORKDIR /prod/app # install the desired package manager RUN npm i -g "pnpm@$MANAGER_VERSION" diff --git a/internal/image/fixtures/test-node_modules-yarn-empty.Dockerfile b/internal/image/fixtures/test-node_modules-yarn-empty.Dockerfile index 7158d5d258..41f4c2f423 100644 --- a/internal/image/fixtures/test-node_modules-yarn-empty.Dockerfile +++ b/internal/image/fixtures/test-node_modules-yarn-empty.Dockerfile @@ -2,7 +2,7 @@ ARG MANAGER_VERSION="1.22.22" FROM node:20-alpine@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c -WORKDIR /usr/app +WORKDIR /prod/app # install the desired package manager RUN npm i -g "yarn@$MANAGER_VERSION" --force diff --git a/internal/image/fixtures/test-node_modules-yarn-full.Dockerfile b/internal/image/fixtures/test-node_modules-yarn-full.Dockerfile index 54889d6804..99e9653f01 100644 --- a/internal/image/fixtures/test-node_modules-yarn-full.Dockerfile +++ b/internal/image/fixtures/test-node_modules-yarn-full.Dockerfile @@ -2,7 +2,7 @@ ARG MANAGER_VERSION="1.22.22" FROM node:20-alpine@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c -WORKDIR /usr/app +WORKDIR /prod/app # install the desired package manager RUN npm i -g "yarn@$MANAGER_VERSION" --force diff --git a/internal/image/image.go b/internal/image/image.go index be3bd3171e..0be6f53bf2 100644 --- a/internal/image/image.go +++ b/internal/image/image.go @@ -11,9 +11,9 @@ import ( "path/filepath" "strings" - "github.com/dghubble/trie" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/osv-scanner/internal/image/pathtree" "github.com/google/osv-scanner/pkg/lockfile" ) @@ -112,7 +112,7 @@ func LoadImage(imagePath string) (*Image, error) { } outputImage.layers[i] = Layer{ - fileNodeTrie: trie.NewPathTrie(), + fileNodeTrie: pathtree.NewNode[FileNode](), id: hash.Hex, rootImage: &outputImage, } @@ -235,7 +235,7 @@ func LoadImage(imagePath string) (*Image, error) { continue } - currentMap.fileNodeTrie.Put(virtualPath, FileNode{ + err := currentMap.fileNodeTrie.Insert(virtualPath, &FileNode{ rootImage: &outputImage, // Select the original layer of the file originLayer: &outputImage.layers[i], @@ -244,6 +244,10 @@ func LoadImage(imagePath string) (*Image, error) { isWhiteout: tombstone, permission: fs.FileMode(header.Mode), //nolint:gosec }) + + if err != nil { + return &outputImage, fmt.Errorf("image tar has repeated files: %w", err) + } } } @@ -260,13 +264,12 @@ func inWhiteoutDir(fileMap Layer, filePath string) bool { if filePath == "" { break } - dirname := filepath.Dir(filePath) + dirname := path.Dir(filePath) if filePath == dirname { break } - val := fileMap.fileNodeTrie.Get(dirname) - item, ok := val.(FileNode) - if ok && item.isWhiteout { + node := fileMap.fileNodeTrie.Get(dirname) + if node != nil && node.isWhiteout { return true } filePath = dirname diff --git a/internal/image/image_test.go b/internal/image/image_test.go index 90bd028524..bc4397ab4e 100644 --- a/internal/image/image_test.go +++ b/internal/image/image_test.go @@ -3,7 +3,6 @@ package image_test import ( "errors" "os" - "sort" "testing" "github.com/google/osv-scanner/internal/image" @@ -94,10 +93,6 @@ func TestScanImage(t *testing.T) { } } - sort.Slice(got.Lockfiles, func(i, j int) bool { - return got.Lockfiles[i].FilePath < got.Lockfiles[j].FilePath - }) - tt.want.MatchJSON(t, got) }) } diff --git a/internal/image/layer.go b/internal/image/layer.go index 9e100dc03f..8539285cc3 100644 --- a/internal/image/layer.go +++ b/internal/image/layer.go @@ -3,9 +3,14 @@ package image import ( "io/fs" "os" + "strings" + "time" + + // Note that paths accessing the disk must use filepath, but all virtual paths should use path + "path" "path/filepath" - "github.com/dghubble/trie" + "github.com/google/osv-scanner/internal/image/pathtree" ) type fileType int @@ -26,6 +31,69 @@ type FileNode struct { permission fs.FileMode } +var _ fs.DirEntry = &FileNode{} + +func (f *FileNode) IsDir() bool { + return f.fileType == Dir +} + +func (f *FileNode) Name() string { + return path.Base(f.virtualPath) +} + +func (f *FileNode) Type() fs.FileMode { + return f.permission +} + +func (f *FileNode) Info() (fs.FileInfo, error) { + return f.Stat() +} + +type FileNodeFileInfo struct { + baseFileInfo fs.FileInfo + fileNode *FileNode +} + +var _ fs.FileInfo = FileNodeFileInfo{} + +func (f FileNodeFileInfo) Name() string { + return path.Base(f.fileNode.virtualPath) +} + +func (f FileNodeFileInfo) Size() int64 { + return f.baseFileInfo.Size() +} + +func (f FileNodeFileInfo) Mode() fs.FileMode { + return f.fileNode.permission +} + +func (f FileNodeFileInfo) ModTime() time.Time { + return f.baseFileInfo.ModTime() +} + +func (f FileNodeFileInfo) IsDir() bool { + return f.fileNode.fileType == Dir +} + +func (f FileNodeFileInfo) Sys() any { + return nil +} + +// Stat returns the FileInfo structure describing file. +func (f *FileNode) Stat() (fs.FileInfo, error) { + baseFileInfo, err := os.Stat(f.absoluteDiskPath()) + if err != nil { + return nil, err + } + + return FileNodeFileInfo{ + baseFileInfo: baseFileInfo, + fileNode: f, + }, nil +} + +// Open returns a file handle for the file func (f *FileNode) Open() (*os.File, error) { if f.isWhiteout { return nil, fs.ErrNotExist @@ -42,35 +110,74 @@ func (f *FileNode) absoluteDiskPath() string { type Layer struct { // id is the sha256 digest of the layer id string - fileNodeTrie *trie.PathTrie + fileNodeTrie *pathtree.Node[FileNode] rootImage *Image // TODO: Use hashmap to speed up path lookups } -func (filemap Layer) getFileNode(path string) (FileNode, error) { - node, ok := filemap.fileNodeTrie.Get(path).(FileNode) - if !ok { - return FileNode{}, fs.ErrNotExist +func (filemap Layer) Open(path string) (fs.File, error) { + node, err := filemap.getFileNode(path) + if err != nil { + return nil, err + } + + return node.Open() +} + +func (filemap Layer) Stat(path string) (fs.FileInfo, error) { + node, err := filemap.getFileNode(path) + if err != nil { + return nil, err + } + + return node.Stat() +} + +func (filemap Layer) ReadDir(path string) ([]fs.DirEntry, error) { + children := filemap.fileNodeTrie.GetChildren(path) + output := make([]fs.DirEntry, 0, len(children)) + for _, node := range children { + output = append(output, node) + } + + return output, nil +} + +var _ fs.FS = Layer{} +var _ fs.StatFS = Layer{} +var _ fs.ReadDirFS = Layer{} + +func (filemap Layer) getFileNode(nodePath string) (*FileNode, error) { + // We expect all paths queried to be absolute paths rooted at the container root + // However, scalibr uses paths without a prepending /, because the paths are relative to Root. + // Root will always be '/' for container scanning, so prepend with / if necessary. + if !strings.HasPrefix(nodePath, "/") { + nodePath = path.Join("/", nodePath) + } + + node := filemap.fileNodeTrie.Get(nodePath) + if node == nil { + return nil, fs.ErrNotExist } return node, nil } // AllFiles return all files that exist on the layer the FileMap is representing -func (filemap Layer) AllFiles() []FileNode { - allFiles := []FileNode{} +func (filemap Layer) AllFiles() []*FileNode { + allFiles := []*FileNode{} // No need to check error since we are not returning any errors - _ = filemap.fileNodeTrie.Walk(func(_ string, value interface{}) error { - node := value.(FileNode) + _ = filemap.fileNodeTrie.Walk(func(_ string, node *FileNode) error { if node.fileType != RegularFile { // Only add regular files return nil } + // TODO: Check if parent is an opaque whiteout if node.isWhiteout { // Don't add whiteout files as they have been deleted return nil } - allFiles = append(allFiles, value.(FileNode)) + allFiles = append(allFiles, node) return nil }) diff --git a/internal/image/pathtree/pathtree.go b/internal/image/pathtree/pathtree.go new file mode 100644 index 0000000000..d14666a5a1 --- /dev/null +++ b/internal/image/pathtree/pathtree.go @@ -0,0 +1,133 @@ +// Package pathtree provides a tree structure for representing file paths. +// Each path segment is a node in the tree, enabling efficient storage +// and retrieval for building virtual file systems. +package pathtree + +import ( + "errors" + "fmt" + "strings" +) + +const divider string = "/" + +var ErrNodeAlreadyExists = errors.New("node already exists") + +// Root node represents the root directory / +type Node[V any] struct { + value *V + children map[string]*Node[V] +} + +func NewNode[V any]() *Node[V] { + return &Node[V]{ + children: make(map[string]*Node[V]), + } +} + +// Insert inserts a value into the tree at the given path. +// If a node already exists at the given path, an error is returned. +// +// If a file is inserted without also inserting the parent directory +// the parent directory entry will have a nil value. +func (node *Node[V]) Insert(path string, value *V) error { + path, err := cleanPath(path) + if err != nil { + return fmt.Errorf("Insert() error: %w", err) + } + + cursor := node + for _, segment := range strings.Split(path, divider) { + next, ok := cursor.children[segment] + // Create the segment if it doesn't exist + if !ok { + next = &Node[V]{ + value: nil, + children: make(map[string]*Node[V]), + } + cursor.children[segment] = next + } + cursor = next + } + + if cursor.value != nil { + return fmt.Errorf("%w: %v", ErrNodeAlreadyExists, divider+path) + } + + cursor.value = value + + return nil +} + +// Get retrieves the value at the given path. +// If no node exists at the given path, nil is returned. +func (node *Node[V]) Get(path string) *V { + path, _ = cleanPath(path) + + cursor := node + for _, segment := range strings.Split(path, divider) { + next, ok := cursor.children[segment] + if !ok { + return nil + } + cursor = next + } + + return cursor.value +} + +// Get retrieves all the direct children of this given path +func (node *Node[V]) GetChildren(path string) []*V { + path, _ = cleanPath(path) + + cursor := node + for _, segment := range strings.Split(path, divider) { + next, ok := cursor.children[segment] + if !ok { + return nil + } + cursor = next + } + + var children = make([]*V, 0, len(cursor.children)) + for _, child := range cursor.children { + // Some entries could be nil if a file is inserted without inserting the + // parent directories. + if child != nil { + children = append(children, child.value) + } + } + + return children +} + +// cleanPath returns a path for use in the tree +// additionally an error is returned if path is not formatted as expected +func cleanPath(inputPath string) (string, error) { + path, found := strings.CutPrefix(inputPath, divider) + if !found { + return "", fmt.Errorf("path %q is not an absolute path", inputPath) + } + path = strings.TrimSuffix(path, "/") + + return path, nil +} + +// Walk walks through all elements of this tree depths first, calling fn at every node +func (node *Node[V]) Walk(fn func(string, *V) error) error { + return node.walk("/", fn) +} + +func (node *Node[V]) walk(path string, fn func(string, *V) error) error { + for key, node := range node.children { + if err := fn(key, node.value); err != nil { + return err + } + err := node.walk(path+divider+key, fn) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/image/pathtree/pathtree_test.go b/internal/image/pathtree/pathtree_test.go new file mode 100644 index 0000000000..556c97545a --- /dev/null +++ b/internal/image/pathtree/pathtree_test.go @@ -0,0 +1,264 @@ +package pathtree_test + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/osv-scanner/internal/image/pathtree" +) + +type testVal struct { + string +} + +func assertNoError(t *testing.T, err error) { + t.Helper() + + if err != nil { + t.Errorf("%v", err) + } +} + +func testTree(t *testing.T) *pathtree.Node[testVal] { + t.Helper() + + tree := pathtree.NewNode[testVal]() + assertNoError(t, tree.Insert("/a", &testVal{"value1"})) + assertNoError(t, tree.Insert("/a/b", &testVal{"value2"})) + assertNoError(t, tree.Insert("/a/b/c", &testVal{"value3"})) + assertNoError(t, tree.Insert("/a/b/d", &testVal{"value4"})) + assertNoError(t, tree.Insert("/a/e", &testVal{"value5"})) + assertNoError(t, tree.Insert("/a/e/f", &testVal{"value6"})) + assertNoError(t, tree.Insert("/a/b/d/f", &testVal{"value7"})) + assertNoError(t, tree.Insert("/a/g", &testVal{"value8"})) + + return tree +} + +func TestNode_Insert_Error(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + tree *pathtree.Node[testVal] + key string + val *testVal + }{ + { + name: "duplicate node", + tree: func() *pathtree.Node[testVal] { + tree := pathtree.NewNode[testVal]() + _ = tree.Insert("/a", &testVal{"value1"}) + + return tree + }(), + key: "/a", + val: &testVal{"value2"}, + }, + { + name: "duplicate node in subtree", + tree: func() *pathtree.Node[testVal] { + tree := pathtree.NewNode[testVal]() + _ = tree.Insert("/a", &testVal{"value1"}) + _ = tree.Insert("/a/b", &testVal{"value2"}) + + return tree + }(), + key: "/a/b", + val: &testVal{"value3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := tt.tree.Insert(tt.key, tt.val) + if err == nil { + t.Errorf("Node.Insert() expected error, got nil") + } + }) + } +} + +func TestNode_Get(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + tree *pathtree.Node[testVal] + key string + want *testVal + }{ + { + name: "empty tree", + tree: pathtree.NewNode[testVal](), + key: "/a", + want: nil, + }, + { + name: "single node", + tree: func() *pathtree.Node[testVal] { + tree := pathtree.NewNode[testVal]() + _ = tree.Insert("/a", &testVal{"value"}) + + return tree + }(), + key: "/a", + want: &testVal{"value"}, + }, + { + name: "non-existent node in single node tree", + tree: func() *pathtree.Node[testVal] { + tree := pathtree.NewNode[testVal]() + _ = tree.Insert("/a", &testVal{"value"}) + + return tree + }(), + key: "/b", + want: nil, + }, + { + name: "multiple nodes", + tree: testTree(t), + key: "/a/b/c", + want: &testVal{"value3"}, + }, + { + name: "non-existent node", + tree: testTree(t), + key: "/a/b/g", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := tt.tree.Get(tt.key) + if diff := cmp.Diff(tt.want, got, cmp.AllowUnexported(testVal{})); diff != "" { + t.Errorf("Node.Get() (-want +got): %v", diff) + } + }) + } +} + +func TestNode_GetChildren(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + tree *pathtree.Node[testVal] + key string + want []*testVal + }{ + { + name: "empty tree", + tree: pathtree.NewNode[testVal](), + key: "/a", + want: nil, + }, + { + name: "single node no children", + tree: func() *pathtree.Node[testVal] { + tree := pathtree.NewNode[testVal]() + _ = tree.Insert("/a", &testVal{"value"}) + + return tree + }(), + key: "/a", + want: []*testVal{}, + }, + { + name: "multiple nodes with children", + tree: testTree(t), + key: "/a/b", + want: []*testVal{ + {"value3"}, + {"value4"}, + }, + }, + { + name: "non-existent node", + tree: testTree(t), + key: "/a/b/g", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := tt.tree.GetChildren(tt.key) + if diff := cmp.Diff( + tt.want, + got, + cmp.AllowUnexported(testVal{}), + cmpopts.SortSlices(func(a, b *testVal) bool { + return strings.Compare(a.string, b.string) < 0 + })); diff != "" { + t.Errorf("Node.GetChildren() (-want +got): %v", diff) + } + }) + } +} + +func TestNode_Walk(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + tree *pathtree.Node[testVal] + want []string + }{ + { + name: "empty tree", + tree: pathtree.NewNode[testVal](), + want: []string{}, + }, + { + name: "single node", + tree: func() *pathtree.Node[testVal] { + tree := pathtree.NewNode[testVal]() + _ = tree.Insert("/a", &testVal{"value"}) + + return tree + }(), + want: []string{"value"}, + }, + { + name: "multiple nodes", + tree: testTree(t), + want: []string{ + "value1", + "value2", + "value3", + "value4", + "value5", + "value6", + "value7", + "value8", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := []string{} + err := tt.tree.Walk(func(_ string, node *testVal) error { + got = append(got, node.string) + return nil + }) + if err != nil { + t.Errorf("Node.Walk() error = %v", err) + } + if diff := cmp.Diff(tt.want, got, cmpopts.SortSlices(func(a, b string) bool { + return strings.Compare(a, b) < 0 + })); diff != "" { + t.Errorf("Node.Walk() (-want +got): %v", diff) + } + }) + } +} diff --git a/internal/image/scan.go b/internal/image/scan.go index 9bfc8ae02d..689e47d481 100644 --- a/internal/image/scan.go +++ b/internal/image/scan.go @@ -1,14 +1,22 @@ package image import ( + "cmp" "errors" "fmt" "io/fs" "log" + "path" + "slices" + "strings" + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem/os/dpkg" + "github.com/google/osv-scanner/internal/lockfilescalibr" "github.com/google/osv-scanner/pkg/lockfile" "github.com/google/osv-scanner/pkg/models" "github.com/google/osv-scanner/pkg/reporter" + "golang.org/x/exp/maps" ) // ScanImage scans an exported docker image .tar file @@ -22,33 +30,120 @@ func ScanImage(r reporter.Reporter, imagePath string) (ScanResults, error) { allFiles := img.LastLayer().AllFiles() - scannedLockfiles := ScanResults{ + scanResults := ScanResults{ ImagePath: imagePath, } + + inventories := []*extractor.Inventory{} + for _, file := range allFiles { if file.fileType != RegularFile { continue } - parsedLockfile, err := extractArtifactDeps(file.virtualPath, img.LastLayer()) + + // TODO: Currently osv-scalibr does not correctly annotate OS packages + // causing artifact extractors to double extract elements here. + // So let's skip all these directories for now. + // See (b/364536788) + // + // https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard + // > Secondary hierarchy for read-only user data; contains the majority of (multi-)user utilities and applications. + // > Should be shareable and read-only. + // + if strings.HasPrefix(file.virtualPath, "/usr/") { + continue + } + + extractedInventories, err := extractArtifactDeps(file.virtualPath, img.LastLayer()) if err != nil { - if !errors.Is(err, lockfile.ErrExtractorNotFound) { + if !errors.Is(err, lockfilescalibr.ErrExtractorNotFound) { r.Errorf("Attempted to extract lockfile but failed: %s - %v\n", file.virtualPath, err) } continue } + inventories = append(inventories, extractedInventories...) + } - scannedLockfiles.Lockfiles = append(scannedLockfiles.Lockfiles, parsedLockfile) + // TODO: Remove the lockfile.Lockfile conversion + // Temporarily convert back to lockfile.Lockfiles to minimize snapshot changes + // This is done to verify the scanning behavior have not changed with this refactor + // and to minimize changes in the initial PR. + lockfiles := map[string]lockfile.Lockfile{} + for _, i := range inventories { + if len(i.Annotations) > 1 { + log.Printf("%v", i.Annotations) + } + lf, exists := lockfiles[path.Join("/", i.Locations[0])] + if !exists { + lf = lockfile.Lockfile{ + FilePath: path.Join("/", i.Locations[0]), + ParsedAs: i.Extractor.Name(), + } + } + + name := i.Name + version := i.Version + + // Debian packages may have a different source name than their package name. + // OSV.dev matches vulnerabilities by source name. + // Convert the given package information to its source information if it is specified. + if metadata, ok := i.Metadata.(*dpkg.Metadata); ok { + if metadata.SourceName != "" { + name = metadata.SourceName + } + if metadata.SourceVersion != "" { + version = metadata.SourceVersion + } + } + + pkg := lockfile.PackageDetails{ + Name: name, + Version: version, + Ecosystem: lockfile.Ecosystem(i.Ecosystem()), + CompareAs: lockfile.Ecosystem(strings.Split(i.Ecosystem(), ":")[0]), + } + if i.SourceCode != nil { + pkg.Commit = i.SourceCode.Commit + } + + lf.Packages = append(lf.Packages, pkg) + + lockfiles[path.Join("/", i.Locations[0])] = lf + } + + for _, l := range lockfiles { + slices.SortFunc(l.Packages, func(a, b lockfile.PackageDetails) int { + return cmp.Or( + strings.Compare(a.Name, b.Name), + strings.Compare(a.Version, b.Version), + ) + }) } - traceOrigin(img, &scannedLockfiles) + scanResults.Lockfiles = maps.Values(lockfiles) + slices.SortFunc(scanResults.Lockfiles, func(a, b lockfile.Lockfile) int { + return strings.Compare(a.FilePath, b.FilePath) + }) + + traceOrigin(img, &scanResults) + + // TODO: Reenable this sort when removing lockfile.Lockfile + // Sort to have deterministic output, and to match behavior of lockfile.extractDeps + // slices.SortFunc(scanResults.Inventories, func(a, b *extractor.Inventory) int { + // // TODO: Should we consider errors here? + // aPURL, _ := a.Extractor.ToPURL(a) + // bPURL, _ := b.Extractor.ToPURL(b) + + // return strings.Compare(aPURL.ToString(), bPURL.ToString()) + // }) err = img.Cleanup() if err != nil { err = fmt.Errorf("failed to cleanup: %w", img.Cleanup()) } - return scannedLockfiles, err + return scanResults, err } // traceOrigin fills out the originLayerID for each package in ScanResults @@ -60,15 +155,30 @@ func traceOrigin(img *Image, scannedLockfiles *ScanResults) { Name string Version string Commit string - Ecosystem lockfile.Ecosystem + Ecosystem string } + // TODO: Remove this function after fully migrating to extractor.Inventory makePDKey := func(pd lockfile.PackageDetails) PDKey { return PDKey{ Name: pd.Name, Version: pd.Version, Commit: pd.Commit, - Ecosystem: pd.Ecosystem, + Ecosystem: string(pd.Ecosystem), + } + } + + makePDKey2 := func(pd *extractor.Inventory) PDKey { + var commit string + if pd.SourceCode != nil { + commit = pd.SourceCode.Commit + } + + return PDKey{ + Name: pd.Name, + Version: pd.Version, + Commit: commit, + Ecosystem: pd.Ecosystem(), } } @@ -120,12 +230,11 @@ func traceOrigin(img *Image, scannedLockfiles *ScanResults) { // Failed to parse an older version of file in image // Behave as if the file does not exist break - // log.Panicf("unimplemented! failed to parse an older version of file in image: %s@%s: %v", file.FilePath, oldFileNode.originLayer.id, err) } // For each package in the old version, check if it existed in the newer layer, if so, the origin must be this layer or earlier. - for _, pkg := range oldDeps.Packages { - key := makePDKey(pkg) + for _, pkg := range oldDeps { + key := makePDKey2(pkg) if val, ok := sourceLayerIdx[key]; ok && val == prevLayerIdx { sourceLayerIdx[key] = layerIdx } diff --git a/internal/lockfilescalibr/errors.go b/internal/lockfilescalibr/errors.go new file mode 100644 index 0000000000..005ee0012b --- /dev/null +++ b/internal/lockfilescalibr/errors.go @@ -0,0 +1,9 @@ +package lockfilescalibr + +import "errors" + +var ErrIncompatibleFileFormat = errors.New("file format is incompatible, but this is expected") +var ErrNotImplemented = errors.New("not implemented") +var ErrWrongExtractor = errors.New("this extractor did not create this inventory") +var ErrExtractorNotFound = errors.New("could not determine extractor") +var ErrNoExtractorsFound = errors.New("no extractors found to be suitable to this file") diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/extractor.go b/internal/lockfilescalibr/language/java/pomxmlnet/extractor.go new file mode 100644 index 0000000000..3a1a5f51c0 --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/extractor.go @@ -0,0 +1,188 @@ +// Package pomxmlnet extracts Maven's pom.xml format with transitive dependency resolution. +package pomxmlnet + +import ( + "context" + "fmt" + "io/fs" + "path/filepath" + + "golang.org/x/exp/maps" + + mavenresolve "deps.dev/util/resolve/maven" + mavenutil "github.com/google/osv-scanner/internal/utility/maven" + + "deps.dev/util/maven" + "deps.dev/util/resolve" + "deps.dev/util/resolve/dep" + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem" + "github.com/google/osv-scalibr/extractor/filesystem/osv" + "github.com/google/osv-scalibr/plugin" + "github.com/google/osv-scalibr/purl" + "github.com/google/osv-scanner/internal/resolution/client" + "github.com/google/osv-scanner/internal/resolution/datasource" +) + +// Extractor extracts osv packages from osv-scanner json output. +type Extractor struct { + client.DependencyClient + *datasource.MavenRegistryAPIClient +} + +// Name of the extractor. +func (e Extractor) Name() string { return "osv/pomxmlnet" } + +// Version of the extractor. +func (e Extractor) Version() int { return 0 } + +// Requirements of the extractor. +func (e Extractor) Requirements() *plugin.Capabilities { + return &plugin.Capabilities{ + Network: true, + } +} + +// FileRequired never returns true, as this is for the osv-scanner json output. +func (e Extractor) FileRequired(path string, _ fs.FileInfo) bool { + return filepath.Base(path) == "pom.xml" +} + +// Extract extracts packages from yarn.lock files passed through the scan input. +func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) ([]*extractor.Inventory, error) { + var project maven.Project + if err := datasource.NewMavenDecoder(input.Reader).Decode(&project); err != nil { + return nil, fmt.Errorf("could not extract from %s: %w", input.Path, err) + } + // Empty JDK and ActivationOS indicates merging the default profiles. + if err := project.MergeProfiles("", maven.ActivationOS{}); err != nil { + return nil, fmt.Errorf("failed to merge profiles: %w", err) + } + for _, repo := range project.Repositories { + if err := e.MavenRegistryAPIClient.AddRegistry(string(repo.URL)); err != nil { + return nil, fmt.Errorf("failed to add registry %s: %w", repo.URL, err) + } + } + // Merging parents data by parsing local parent pom.xml or fetching from upstream. + if err := mavenutil.MergeParents(ctx, e.MavenRegistryAPIClient, &project, project.Parent, 1, input.Path, true); err != nil { + return nil, fmt.Errorf("failed to merge parents: %w", err) + } + // Process the dependencies: + // - dedupe dependencies and dependency management + // - import dependency management + // - fill in missing dependency version requirement + project.ProcessDependencies(func(groupID, artifactID, version maven.String) (maven.DependencyManagement, error) { + return mavenutil.GetDependencyManagement(ctx, e.MavenRegistryAPIClient, groupID, artifactID, version) + }) + + if registries := e.MavenRegistryAPIClient.GetRegistries(); len(registries) > 0 { + clientRegs := make([]client.Registry, len(registries)) + for i, reg := range registries { + clientRegs[i] = client.Registry{URL: reg} + } + if err := e.DependencyClient.AddRegistries(clientRegs); err != nil { + return nil, err + } + } + + overrideClient := client.NewOverrideClient(e.DependencyClient) + resolver := mavenresolve.NewResolver(overrideClient) + + // Resolve the dependencies. + root := resolve.Version{ + VersionKey: resolve.VersionKey{ + PackageKey: resolve.PackageKey{ + System: resolve.Maven, + Name: project.ProjectKey.Name(), + }, + VersionType: resolve.Concrete, + Version: string(project.Version), + }} + reqs := make([]resolve.RequirementVersion, len(project.Dependencies)+len(project.DependencyManagement.Dependencies)) + for i, d := range project.Dependencies { + reqs[i] = resolve.RequirementVersion{ + VersionKey: resolve.VersionKey{ + PackageKey: resolve.PackageKey{ + System: resolve.Maven, + Name: d.Name(), + }, + VersionType: resolve.Requirement, + Version: string(d.Version), + }, + Type: resolve.MavenDepType(d, ""), + } + } + for i, d := range project.DependencyManagement.Dependencies { + reqs[len(project.Dependencies)+i] = resolve.RequirementVersion{ + VersionKey: resolve.VersionKey{ + PackageKey: resolve.PackageKey{ + System: resolve.Maven, + Name: d.Name(), + }, + VersionType: resolve.Requirement, + Version: string(d.Version), + }, + Type: resolve.MavenDepType(d, mavenutil.OriginManagement), + } + } + overrideClient.AddVersion(root, reqs) + + client.PreFetch(ctx, overrideClient, reqs, input.Path) + g, err := resolver.Resolve(ctx, root.VersionKey) + if err != nil { + return nil, fmt.Errorf("failed resolving %v: %w", root, err) + } + for i, e := range g.Edges { + e.Type = dep.Type{} + g.Edges[i] = e + } + + details := map[string]*extractor.Inventory{} + for i := 1; i < len(g.Nodes); i++ { + // Ignore the first node which is the root. + node := g.Nodes[i] + depGroups := []string{} + inventory := extractor.Inventory{ + Name: node.Version.Name, + Version: node.Version.Version, + // TODO(rexpan): Add merged paths in here as well + Locations: []string{input.Path}, + } + // We are only able to know dependency groups of direct dependencies but + // not transitive dependencies because the nodes in the resolve graph does + // not have the scope information. + for _, dep := range project.Dependencies { + if dep.Name() != inventory.Name { + continue + } + if dep.Scope != "" && dep.Scope != "compile" { + depGroups = append(depGroups, string(dep.Scope)) + } + } + inventory.Metadata = osv.DepGroupMetadata{ + DepGroupVals: depGroups, + } + details[inventory.Name] = &inventory + } + + return maps.Values(details), nil +} + +// ToPURL converts an inventory created by this extractor into a PURL. +func (e Extractor) ToPURL(i *extractor.Inventory) *purl.PackageURL { + return &purl.PackageURL{ + Type: purl.TypeMaven, + Name: i.Name, + Version: i.Version, + } +} + +// ToCPEs is not applicable as this extractor does not infer CPEs from the Inventory. +func (e Extractor) ToCPEs(_ *extractor.Inventory) []string { return []string{} } + +// Ecosystem returns the OSV ecosystem ('npm') of the software extracted by this extractor. +func (e Extractor) Ecosystem(_ *extractor.Inventory) string { + return "Maven" +} + +var _ filesystem.Extractor = Extractor{} diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/extractor_test.go b/internal/lockfilescalibr/language/java/pomxmlnet/extractor_test.go new file mode 100644 index 0000000000..556663be75 --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/extractor_test.go @@ -0,0 +1,366 @@ +package pomxmlnet_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem/osv" + "github.com/google/osv-scalibr/testing/extracttest" + "github.com/google/osv-scanner/internal/lockfilescalibr/language/java/pomxmlnet" + "github.com/google/osv-scanner/internal/resolution/clienttest" + "github.com/google/osv-scanner/internal/resolution/datasource" + "github.com/google/osv-scanner/internal/testutility" +) + +func TestMavenResolverExtractor_FileRequired(t *testing.T) { + t.Parallel() + + tests := []struct { + path string + want bool + }{ + { + path: "", + want: false, + }, + { + path: "pom.xml", + want: true, + }, + { + path: "path/to/my/pom.xml", + want: true, + }, + { + path: "path/to/my/pom.xml/file", + want: false, + }, + { + path: "path/to/my/pom.xml.file", + want: false, + }, + { + path: "path.to.my.pom.xml", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + t.Parallel() + e := pomxmlnet.Extractor{} + got := e.FileRequired(tt.path, nil) + if got != tt.want { + t.Errorf("Extract() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestExtractor_Extract(t *testing.T) { + t.Parallel() + + tests := []extracttest.TestTableEntry{ + { + Name: "Not a pom file", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/not-pom.txt", + }, + WantErr: extracttest.ContainsErrStr{Str: "could not extract from"}, + }, + { + Name: "invalid xml syntax", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/invalid-syntax.xml", + }, + WantErr: extracttest.ContainsErrStr{Str: "XML syntax error"}, + }, + { + Name: "empty", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/empty.xml", + }, + WantInventory: []*extractor.Inventory{}, + }, + { + Name: "one package", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/one-package.xml", + }, + WantInventory: []*extractor.Inventory{ + { + Name: "org.apache.maven:maven-artifact", + Version: "1.0.0", + Locations: []string{"testdata/maven/one-package.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + }, + }, + { + Name: "two packages", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/two-packages.xml", + }, + WantInventory: []*extractor.Inventory{ + { + Name: "io.netty:netty-all", + Version: "4.1.42.Final", + Locations: []string{"testdata/maven/two-packages.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.slf4j:slf4j-log4j12", + Version: "1.7.25", + Locations: []string{"testdata/maven/two-packages.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + }, + }, + { + Name: "with dependency management", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/with-dependency-management.xml", + }, + WantInventory: []*extractor.Inventory{ + { + Name: "io.netty:netty-all", + Version: "4.1.9", + Locations: []string{"testdata/maven/with-dependency-management.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.slf4j:slf4j-log4j12", + Version: "1.7.25", + Locations: []string{"testdata/maven/with-dependency-management.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + }, + }, + { + Name: "interpolation", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/interpolation.xml", + }, + WantInventory: []*extractor.Inventory{ + { + Name: "org.mine:mypackage", + Version: "1.0.0", + Locations: []string{"testdata/maven/interpolation.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.mine:my.package", + Version: "2.3.4", + Locations: []string{"testdata/maven/interpolation.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.mine:ranged-package", + Version: "9.4.37", + Locations: []string{"testdata/maven/interpolation.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + }, + }, + { + Name: "with scope / dep groups", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/with-scope.xml", + }, + WantInventory: []*extractor.Inventory{ + { + Name: "junit:junit", + Version: "4.12", + Locations: []string{"testdata/maven/with-scope.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{"runtime"}}, + }, + }, + }, + { + Name: "transitive dependencies", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/transitive.xml", + }, + WantInventory: []*extractor.Inventory{ + { + Name: "org.direct:alice", + Version: "1.0.0", + Locations: []string{"testdata/maven/transitive.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.direct:bob", + Version: "2.0.0", + Locations: []string{"testdata/maven/transitive.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.direct:chris", + Version: "3.0.0", + Locations: []string{"testdata/maven/transitive.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.transitive:chuck", + Version: "1.1.1", + Locations: []string{"testdata/maven/transitive.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.transitive:dave", + Version: "2.2.2", + Locations: []string{"testdata/maven/transitive.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.transitive:eve", + Version: "3.3.3", + Locations: []string{"testdata/maven/transitive.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.transitive:frank", + Version: "4.4.4", + Locations: []string{"testdata/maven/transitive.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + resolutionClient := clienttest.NewMockResolutionClient(t, "testdata/universe/basic-universe.yaml") + extr := pomxmlnet.Extractor{ + DependencyClient: resolutionClient, + MavenRegistryAPIClient: &datasource.MavenRegistryAPIClient{}, + } + + scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig) + defer extracttest.CloseTestScanInput(t, scanInput) + + got, err := extr.Extract(context.Background(), &scanInput) + + if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) + return + } + + if diff := cmp.Diff(tt.WantInventory, got, cmpopts.SortSlices(extracttest.InventoryCmpLess)); diff != "" { + t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) + } + }) + } +} + +func TestExtractor_Extract_WithMockServer(t *testing.T) { + t.Parallel() + + tt := extracttest.TestTableEntry{ + // Name: "with parent", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/maven/with-parent.xml", + }, + WantInventory: []*extractor.Inventory{ + { + Name: "org.alice:alice", + Version: "1.0.0", + Locations: []string{"testdata/maven/with-parent.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.bob:bob", + Version: "2.0.0", + Locations: []string{"testdata/maven/with-parent.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.chuck:chuck", + Version: "3.0.0", + Locations: []string{"testdata/maven/with-parent.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.dave:dave", + Version: "4.0.0", + Locations: []string{"testdata/maven/with-parent.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.eve:eve", + Version: "5.0.0", + Locations: []string{"testdata/maven/with-parent.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + { + Name: "org.frank:frank", + Version: "6.0.0", + Locations: []string{"testdata/maven/with-parent.xml"}, + Metadata: osv.DepGroupMetadata{DepGroupVals: []string{}}, + }, + }, + } + + srv := testutility.NewMockHTTPServer(t) + srv.SetResponse(t, "org/upstream/parent-pom/1.0/parent-pom-1.0.pom", []byte(` + <project> + <groupId>org.upstream</groupId> + <artifactId>parent-pom</artifactId> + <version>1.0</version> + <packaging>pom</packaging> + <dependencies> + <dependency> + <groupId>org.eve</groupId> + <artifactId>eve</artifactId> + <version>5.0.0</version> + </dependency> + </dependencies> + </project> + `)) + srv.SetResponse(t, "org/import/import/1.2.3/import-1.2.3.pom", []byte(` + <project> + <groupId>org.import</groupId> + <artifactId>import</artifactId> + <version>1.2.3</version> + <packaging>pom</packaging> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.frank</groupId> + <artifactId>frank</artifactId> + <version>6.0.0</version> + </dependency> + </dependencies> + </dependencyManagement> + </project> + `)) + + apiClient, err := datasource.NewMavenRegistryAPIClient(srv.URL) + if err != nil { + t.Fatalf("%v", err) + } + + resolutionClient := clienttest.NewMockResolutionClient(t, "testdata/universe/basic-universe.yaml") + extr := pomxmlnet.Extractor{ + DependencyClient: resolutionClient, + MavenRegistryAPIClient: apiClient, + } + + scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig) + defer extracttest.CloseTestScanInput(t, scanInput) + + got, err := extr.Extract(context.Background(), &scanInput) + + if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) + return + } + + if diff := cmp.Diff(tt.WantInventory, got, cmpopts.SortSlices(extracttest.InventoryCmpLess)); diff != "" { + t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) + } +} diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/empty.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/empty.xml new file mode 100644 index 0000000000..8cfeebaaa4 --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/empty.xml @@ -0,0 +1,7 @@ +<project> + <modelVersion>4.0.0</modelVersion> + + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <version>1</version> +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/interpolation.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/interpolation.xml new file mode 100644 index 0000000000..6b7f761afc --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/interpolation.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>io.library</groupId> + <artifactId>my-library</artifactId> + <version>1.0-SNAPSHOT</version> + <packaging>jar</packaging> + + <properties> + <mypackageVersion>1.0.0</mypackageVersion> + <my.package.version>2.3.4</my.package.version> + <version-range>[9.4.35.v20201120,9.5)</version-range> + </properties> + + <dependencies> + <dependency> + <groupId>org.mine</groupId> + <artifactId>mypackage</artifactId> + <version>${mypackageVersion}</version> + </dependency> + + <dependency> + <groupId>org.mine</groupId> + <artifactId>my.package</artifactId> + <version>${my.package.version}</version> + </dependency> + + <dependency> + <groupId>org.mine</groupId> + <artifactId>ranged-package</artifactId> + <version>${version-range}</version> + </dependency> + </dependencies> + +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/invalid-syntax.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/invalid-syntax.xml new file mode 100644 index 0000000000..761a32c1ab --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/invalid-syntax.xml @@ -0,0 +1,13 @@ +<project> + <properties> + <${Id}.version>${project.version}</${Id}.version> + </properties> + + <dependencies> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-all</artifactId> + <version>4.1.42.Final</version> + </dependency> + </dependencies> +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/not-pom.txt b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/not-pom.txt new file mode 100644 index 0000000000..f9df712bcb --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/not-pom.txt @@ -0,0 +1 @@ +this is not a pom.xml file! diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/one-package.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/one-package.xml new file mode 100644 index 0000000000..bbb1359e9d --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/one-package.xml @@ -0,0 +1,17 @@ +<project> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <version>1.0</version> + + <properties> + <mavenVersion>3.0</mavenVersion> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.maven</groupId> + <artifactId>maven-artifact</artifactId> + <version>1.0.0</version> + </dependency> + </dependencies> +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/parent/pom.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/parent/pom.xml new file mode 100644 index 0000000000..3751df6be3 --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/parent/pom.xml @@ -0,0 +1,21 @@ +<project> + <groupId>org.local</groupId> + <artifactId>parent-pom</artifactId> + <version>1.0</version> + + <packaging>pom</packaging> + + <parent> + <groupId>org.upstream</groupId> + <artifactId>parent-pom</artifactId> + <version>1.0</version> + </parent> + + <dependencies> + <dependency> + <groupId>org.dave</groupId> + <artifactId>dave</artifactId> + <version>4.0.0</version> + </dependency> + </dependencies> +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/transitive.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/transitive.xml new file mode 100644 index 0000000000..52e416a0bc --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/transitive.xml @@ -0,0 +1,33 @@ +<project> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <version>1.0</version> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.transitive</groupId> + <artifactId>frank</artifactId> + <version>4.4.4</version> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.direct</groupId> + <artifactId>alice</artifactId> + <version>1.0.0</version> + </dependency> + <dependency> + <groupId>org.direct</groupId> + <artifactId>bob</artifactId> + <version>2.0.0</version> + </dependency> + <dependency> + <groupId>org.direct</groupId> + <artifactId>chris</artifactId> + <version>3.0.0</version> + </dependency> + </dependencies> +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/two-packages.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/two-packages.xml new file mode 100644 index 0000000000..897f648a1e --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/two-packages.xml @@ -0,0 +1,22 @@ +<project> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <version>1.0</version> + + <properties> + <mavenVersion>3.0</mavenVersion> + </properties> + + <dependencies> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-all</artifactId> + <version>4.1.42.Final</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + <version>1.7.25</version> + </dependency> + </dependencies> +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-dependency-management.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-dependency-management.xml new file mode 100644 index 0000000000..1928688e94 --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-dependency-management.xml @@ -0,0 +1,37 @@ +<project> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <version>1.0</version> + + <properties> + <mavenVersion>3.0</mavenVersion> + </properties> + + <dependencies> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-all</artifactId> + <version>4.1.9</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + <version>1.7.25</version> + </dependency> + </dependencies> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-all</artifactId> + <version>4.1.42.Final</version> + </dependency> + <dependency> + <groupId>com.google.code.findbugs</groupId> + <artifactId>jsr305</artifactId> + <version>3.0.2</version> + </dependency> + </dependencies> + </dependencyManagement> +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-parent.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-parent.xml new file mode 100644 index 0000000000..602b8b877f --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-parent.xml @@ -0,0 +1,54 @@ +<project> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <version>1.0</version> + + <parent> + <groupId>org.local</groupId> + <artifactId>parent-pom</artifactId> + <version>1.0</version> + <relativePath>./parent/pom.xml</relativePath> + </parent> + + <properties> + <bob.version>2.0.0</bob.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.alice</groupId> + <artifactId>alice</artifactId> + <version>1.0.0</version> + </dependency> + <dependency> + <groupId>org.bob</groupId> + <artifactId>bob</artifactId> + <version>${bob.version}</version> + </dependency> + <dependency> + <groupId>org.chuck</groupId> + <artifactId>chuck</artifactId> + </dependency> + <dependency> + <groupId>org.frank</groupId> + <artifactId>frank</artifactId> + </dependency> + </dependencies> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.chuck</groupId> + <artifactId>chuck</artifactId> + <version>3.0.0</version> + </dependency> + <dependency> + <groupId>org.import</groupId> + <artifactId>import</artifactId> + <version>1.2.3</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-scope.xml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-scope.xml new file mode 100644 index 0000000000..688c6bb7bc --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/maven/with-scope.xml @@ -0,0 +1,14 @@ +<project> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <version>1.0</version> + + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>runtime</scope> + </dependency> + </dependencies> +</project> diff --git a/internal/lockfilescalibr/language/java/pomxmlnet/testdata/universe/basic-universe.yaml b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/universe/basic-universe.yaml new file mode 100644 index 0000000000..2bf2b32724 --- /dev/null +++ b/internal/lockfilescalibr/language/java/pomxmlnet/testdata/universe/basic-universe.yaml @@ -0,0 +1,60 @@ +system: maven +schema: | + com.google.code.findbugs:jsr305 + 3.0.2 + io.netty:netty-all + 4.1.9 + 4.1.42.Final + junit:junit + 4.12 + org.alice:alice + 1.0.0 + org.apache.maven:maven-artifact + 1.0.0 + org.bob:bob + 2.0.0 + org.chuck:chuck + 3.0.0 + org.dave:dave + 4.0.0 + org.direct:alice + 1.0.0 + org.transitive:chuck@1.1.1 + org.transitive:dave@2.2.2 + org.direct:bob + 2.0.0 + org.transitive:eve@3.3.3 + org.direct:chris + 3.0.0 + org.transitive:frank@3.3.3 + org.eve:eve + 5.0.0 + org.frank:frank + 6.0.0 + org.mine:my.package + 2.3.4 + org.mine:mypackage + 1.0.0 + org.mine:ranged-package + 9.4.35 + 9.4.36 + 9.4.37 + 9.5 + org.slf4j:slf4j-log4j12 + 1.7.25 + org.transitive:chuck + 1.1.1 + 2.2.2 + org.transitive:eve@2.2.2 + 3.3.3 + org.transitive:dave + 1.1.1 + 2.2.2 + 3.3.3 + org.transitive:eve + 1.1.1 + 2.2.2 + 3.3.3 + org.transitive:frank + 3.3.3 + 4.4.4 diff --git a/internal/lockfilescalibr/language/javascript/nodemodules/extractor.go b/internal/lockfilescalibr/language/javascript/nodemodules/extractor.go new file mode 100644 index 0000000000..a965b2fecd --- /dev/null +++ b/internal/lockfilescalibr/language/javascript/nodemodules/extractor.go @@ -0,0 +1,57 @@ +package nodemodules + +import ( + "context" + "io/fs" + "path/filepath" + + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem" + "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagelockjson" + "github.com/google/osv-scalibr/plugin" + "github.com/google/osv-scalibr/purl" +) + +type Extractor struct { + actualExtractor packagelockjson.Extractor +} + +var _ filesystem.Extractor = Extractor{} + +// Name of the extractor. +func (e Extractor) Name() string { return "javascript/nodemodules" } + +// Version of the extractor. +func (e Extractor) Version() int { return 0 } + +// Requirements of the extractor. +func (e Extractor) Requirements() *plugin.Capabilities { + return &plugin.Capabilities{} +} + +// FileRequired returns true for .package-lock.json files under node_modules +func (e Extractor) FileRequired(path string, _ fs.FileInfo) bool { + return filepath.Base(filepath.Dir(path)) == "node_modules" && filepath.Base(path) == ".package-lock.json" +} + +// Extract extracts packages from yarn.lock files passed through the scan input. +func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) ([]*extractor.Inventory, error) { + return e.actualExtractor.Extract(ctx, input) +} + +// ToPURL converts an inventory created by this extractor into a PURL. +func (e Extractor) ToPURL(i *extractor.Inventory) *purl.PackageURL { + return e.actualExtractor.ToPURL(i) +} + +// ToCPEs is not applicable as this extractor does not infer CPEs from the Inventory. +func (e Extractor) ToCPEs(i *extractor.Inventory) []string { + return e.actualExtractor.ToCPEs(i) +} + +// Ecosystem returns the OSV ecosystem ('npm') of the software extracted by this extractor. +func (e Extractor) Ecosystem(i *extractor.Inventory) string { + return e.actualExtractor.Ecosystem(i) +} + +var _ filesystem.Extractor = Extractor{} diff --git a/internal/lockfilescalibr/language/osv/osvscannerjson/extractor.go b/internal/lockfilescalibr/language/osv/osvscannerjson/extractor.go new file mode 100644 index 0000000000..27de9b2580 --- /dev/null +++ b/internal/lockfilescalibr/language/osv/osvscannerjson/extractor.go @@ -0,0 +1,84 @@ +// Package osvscannerjson extracts osv-scanner's json output. +package osvscannerjson + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem" + "github.com/google/osv-scalibr/plugin" + "github.com/google/osv-scalibr/purl" + "github.com/google/osv-scanner/pkg/models" +) + +// Extractor extracts osv packages from osv-scanner json output. +type Extractor struct{} + +// Name of the extractor. +func (e Extractor) Name() string { return "osv/osvscannerjson" } + +// Version of the extractor. +func (e Extractor) Version() int { return 0 } + +// Requirements of the extractor. +func (e Extractor) Requirements() *plugin.Capabilities { + return &plugin.Capabilities{} +} + +// FileRequired never returns true, as this is for the osv-scanner json output. +func (e Extractor) FileRequired(_ string, _ fs.FileInfo) bool { + return false +} + +// Extract extracts packages from yarn.lock files passed through the scan input. +func (e Extractor) Extract(_ context.Context, input *filesystem.ScanInput) ([]*extractor.Inventory, error) { + parsedResults := models.VulnerabilityResults{} + err := json.NewDecoder(input.Reader).Decode(&parsedResults) + + if err != nil { + return nil, fmt.Errorf("could not extract from %s: %w", input.Path, err) + } + + packages := []*extractor.Inventory{} + for _, res := range parsedResults.Results { + for _, pkg := range res.Packages { + inventory := extractor.Inventory{ + Name: pkg.Package.Name, + Version: pkg.Package.Version, + Metadata: Metadata{ + Ecosystem: pkg.Package.Ecosystem, + SourceInfo: res.Source, + }, + Locations: []string{input.Path}, + } + if pkg.Package.Commit != "" { + inventory.SourceCode = &extractor.SourceCodeIdentifier{ + Commit: pkg.Package.Commit, + } + } + + packages = append(packages, &inventory) + } + } + + return packages, nil +} + +// ToPURL converts an inventory created by this extractor into a PURL. +func (e Extractor) ToPURL(_ *extractor.Inventory) *purl.PackageURL { + // TODO: support purl conversion + return nil +} + +// ToCPEs is not applicable as this extractor does not infer CPEs from the Inventory. +func (e Extractor) ToCPEs(_ *extractor.Inventory) []string { return []string{} } + +// Ecosystem returns the OSV ecosystem ('npm') of the software extracted by this extractor. +func (e Extractor) Ecosystem(i *extractor.Inventory) string { + return i.Metadata.(Metadata).Ecosystem +} + +var _ filesystem.Extractor = Extractor{} diff --git a/internal/lockfilescalibr/language/osv/osvscannerjson/extractor_test.go b/internal/lockfilescalibr/language/osv/osvscannerjson/extractor_test.go new file mode 100644 index 0000000000..65289c4d4c --- /dev/null +++ b/internal/lockfilescalibr/language/osv/osvscannerjson/extractor_test.go @@ -0,0 +1,139 @@ +package osvscannerjson_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/testing/extracttest" + "github.com/google/osv-scanner/internal/lockfilescalibr/language/osv/osvscannerjson" + "github.com/google/osv-scanner/pkg/models" +) + +func TestExtractor_Extract(t *testing.T) { + t.Parallel() + + tests := []extracttest.TestTableEntry{ + { + Name: "invalid yaml", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/not-json.txt", + }, + WantErr: extracttest.ContainsErrStr{Str: "could not extract from"}, + }, + { + Name: "empty", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/empty.json", + }, + WantInventory: []*extractor.Inventory{}, + }, + { + Name: "one package", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/one-package.json", + }, + WantInventory: []*extractor.Inventory{ + { + Name: "activesupport", + Version: "7.0.7", + Locations: []string{"testdata/one-package.json"}, + Metadata: osvscannerjson.Metadata{ + Ecosystem: "RubyGems", + SourceInfo: models.SourceInfo{ + Path: "/path/to/Gemfile.lock", + Type: "lockfile", + }, + }, + }, + }, + }, + { + Name: "one package with commit", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/one-package-commit.json", + }, + WantInventory: []*extractor.Inventory{ + { + Locations: []string{"testdata/one-package-commit.json"}, + SourceCode: &extractor.SourceCodeIdentifier{ + Commit: "9a6bd55c9d0722cb101fe85a3b22d89e4ff4fe52", + }, + Metadata: osvscannerjson.Metadata{ + SourceInfo: models.SourceInfo{ + Path: "/path/to/Gemfile.lock", + Type: "lockfile", + }, + }, + }, + }, + }, + { + Name: "multiple packages", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/multiple-packages-with-vulns.json", + }, + WantInventory: []*extractor.Inventory{ + { + Name: "crossbeam-utils", + Version: "0.6.6", + Locations: []string{"testdata/multiple-packages-with-vulns.json"}, + Metadata: osvscannerjson.Metadata{ + Ecosystem: "crates.io", + SourceInfo: models.SourceInfo{ + Path: "/path/to/Cargo.lock", + Type: "lockfile", + }, + }, + }, + { + Name: "memoffset", + Version: "0.5.6", + Locations: []string{"testdata/multiple-packages-with-vulns.json"}, + Metadata: osvscannerjson.Metadata{ + Ecosystem: "crates.io", + SourceInfo: models.SourceInfo{ + Path: "/path/to/Cargo.lock", + Type: "lockfile", + }, + }, + }, + { + Name: "smallvec", + Version: "1.6.0", + Locations: []string{"testdata/multiple-packages-with-vulns.json"}, + Metadata: osvscannerjson.Metadata{ + Ecosystem: "crates.io", + SourceInfo: models.SourceInfo{ + Path: "/path/to/Cargo.lock", + Type: "lockfile", + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + extr := osvscannerjson.Extractor{} + + scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig) + defer extracttest.CloseTestScanInput(t, scanInput) + + got, err := extr.Extract(context.Background(), &scanInput) + + if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) + return + } + + if diff := cmp.Diff(tt.WantInventory, got, cmpopts.SortSlices(extracttest.InventoryCmpLess)); diff != "" { + t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) + } + }) + } +} diff --git a/internal/lockfilescalibr/language/osv/osvscannerjson/metadata.go b/internal/lockfilescalibr/language/osv/osvscannerjson/metadata.go new file mode 100644 index 0000000000..45c9e2c966 --- /dev/null +++ b/internal/lockfilescalibr/language/osv/osvscannerjson/metadata.go @@ -0,0 +1,9 @@ +package osvscannerjson + +import "github.com/google/osv-scanner/pkg/models" + +// Metadata holds the metadata for osvscanner.json +type Metadata struct { + Ecosystem string + SourceInfo models.SourceInfo +} diff --git a/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/empty.json b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/empty.json new file mode 100644 index 0000000000..a9452a2e2b --- /dev/null +++ b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/empty.json @@ -0,0 +1,3 @@ +{ + "results": [] +} \ No newline at end of file diff --git a/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/multiple-packages-with-vulns.json b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/multiple-packages-with-vulns.json new file mode 100644 index 0000000000..c861029368 --- /dev/null +++ b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/multiple-packages-with-vulns.json @@ -0,0 +1,504 @@ +{ + "results": [ + { + "source": { + "path": "/path/to/Cargo.lock", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "crossbeam-utils", + "version": "0.6.6", + "ecosystem": "crates.io" + }, + "vulnerabilities": [ + { + "modified": "2022-08-11T21:55:33Z", + "published": "2022-02-16T22:36:21Z", + "schema_version": "1.4.0", + "id": "GHSA-qc84-gqf4-9926", + "aliases": [ + "CVE-2022-23639" + ], + "summary": "crossbeam-utils Race Condition vulnerability", + "details": "### Impact\n\nThe affected version of this crate incorrectly assumed that the alignment of `{i,u}64` was always the same as `Atomic{I,U}64`. \n\nHowever, the alignment of `{i,u}64` on a 32-bit target can be smaller than `Atomic{I,U}64`.\n\nThis can cause the following problems:\n\n- Unaligned memory accesses\n- Data race\n\nCrates using `fetch_*` methods with `AtomicCell\u003c{i,u}64\u003e` are affected by this issue.\n\n32-bit targets without `Atomic{I,U}64` and 64-bit targets are not affected by this issue.\n32-bit targets with `Atomic{I,U}64` and `{i,u}64` have the same alignment are also not affected by this issue.\n\nThe following is a complete list of the builtin targets that may be affected. (last update: nightly-2022-02-11)\n\n- armv7-apple-ios (tier 3)\n- armv7s-apple-ios (tier 3)\n- i386-apple-ios (tier 3)\n- i586-unknown-linux-gnu\n- i586-unknown-linux-musl\n- i686-apple-darwin (tier 3)\n- i686-linux-android\n- i686-unknown-freebsd\n- i686-unknown-haiku (tier 3)\n- i686-unknown-linux-gnu\n- i686-unknown-linux-musl\n- i686-unknown-netbsd (tier 3)\n- i686-unknown-openbsd (tier 3)\n- i686-wrs-vxworks (tier 3)\n\n([script to get list](https://gist.github.com/taiki-e/3c7891e8c5f5e0cbcb44d7396aabfe10))\n\n### Patches\n\nThis has been fixed in crossbeam-utils 0.8.7.\n\nAffected 0.8.x releases have been yanked.\n\n### References\n\nhttps://github.com/crossbeam-rs/crossbeam/pull/781 \n\n### License\n\nThis advisory is in the public domain.", + "affected": [ + { + "package": { + "ecosystem": "crates.io", + "name": "crossbeam-utils", + "purl": "pkg:cargo/crossbeam-utils" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.8.7" + } + ] + } + ], + "database_specific": { + "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2022/02/GHSA-qc84-gqf4-9926/GHSA-qc84-gqf4-9926.json" + } + } + ], + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "references": [ + { + "type": "WEB", + "url": "https://github.com/crossbeam-rs/crossbeam/security/advisories/GHSA-qc84-gqf4-9926" + }, + { + "type": "ADVISORY", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-23639" + }, + { + "type": "WEB", + "url": "https://github.com/crossbeam-rs/crossbeam/pull/781" + }, + { + "type": "PACKAGE", + "url": "https://github.com/crossbeam-rs/crossbeam" + }, + { + "type": "WEB", + "url": "https://github.com/crossbeam-rs/crossbeam/releases/tag/crossbeam-utils-0.8.7" + }, + { + "type": "WEB", + "url": "https://rustsec.org/advisories/RUSTSEC-2022-0041.html" + } + ], + "database_specific": { + "cwe_ids": [ + "CWE-362" + ], + "github_reviewed": true, + "github_reviewed_at": "2022-02-16T22:36:21Z", + "nvd_published_at": "2022-02-15T19:15:00Z", + "severity": "HIGH" + } + }, + { + "modified": "2022-08-04T13:56:30Z", + "published": "2022-02-05T12:00:00Z", + "schema_version": "1.4.0", + "id": "RUSTSEC-2022-0041", + "aliases": [ + "GHSA-qc84-gqf4-9926", + "CVE-2022-23639" + ], + "summary": "Unsoundness of AtomicCell\u003c*64\u003e arithmetics on 32-bit targets that support Atomic*64", + "details": "## Impact\n\nAffected versions of this crate incorrectly assumed that the alignment of {i,u}64 was always the same as Atomic{I,U}64.\n\nHowever, the alignment of {i,u}64 on a 32-bit target can be smaller than Atomic{I,U}64.\n\nThis can cause the following problems:\n\n- Unaligned memory accesses\n- Data race\n\nCrates using fetch_* methods with AtomicCell\u003c{i,u}64\u003e are affected by this issue.\n\n32-bit targets without Atomic{I,U}64 and 64-bit targets are not affected by this issue.\n\n32-bit targets with Atomic{I,U}64 and {i,u}64 have the same alignment are also not affected by this issue.\n\nThe following is a complete list of the builtin targets that may be affected. (last update: nightly-2022-02-11)\n\n- armv7-apple-ios (tier 3)\n- armv7s-apple-ios (tier 3)\n- i386-apple-ios (tier 3)\n- i586-unknown-linux-gnu\n- i586-unknown-linux-musl\n- i686-apple-darwin (tier 3)\n- i686-linux-android\n- i686-unknown-freebsd\n- i686-unknown-haiku (tier 3)\n- i686-unknown-linux-gnu\n- i686-unknown-linux-musl\n- i686-unknown-netbsd (tier 3)\n- i686-unknown-openbsd (tier 3)\n- i686-wrs-vxworks (tier 3)\n\n([script to get list](https://gist.github.com/taiki-e/3c7891e8c5f5e0cbcb44d7396aabfe10))\n\n## Patches\n\nThis has been fixed in crossbeam-utils 0.8.7.\n\nAffected 0.8.x releases have been yanked.\n\nThanks to @taiki-e", + "affected": [ + { + "package": { + "ecosystem": "crates.io", + "name": "crossbeam-utils", + "purl": "pkg:cargo/crossbeam-utils" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.8.7" + } + ] + } + ], + "database_specific": { + "categories": [ + "memory-corruption" + ], + "cvss": null, + "informational": "unsound", + "source": "https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2022-0041.json" + }, + "ecosystem_specific": { + "affects": { + "arch": [], + "functions": [], + "os": [] + } + } + } + ], + "references": [ + { + "type": "PACKAGE", + "url": "https://crates.io/crates/crossbeam-utils" + }, + { + "type": "ADVISORY", + "url": "https://rustsec.org/advisories/RUSTSEC-2022-0041.html" + }, + { + "type": "WEB", + "url": "https://github.com/crossbeam-rs/crossbeam/pull/781" + } + ] + } + ], + "groups": [ + { + "ids": [ + "GHSA-qc84-gqf4-9926", + "RUSTSEC-2022-0041" + ] + } + ] + }, + { + "package": { + "name": "memoffset", + "version": "0.5.6", + "ecosystem": "crates.io" + }, + "vulnerabilities": [ + { + "modified": "2023-06-21T22:06:29Z", + "published": "2023-06-21T22:06:29Z", + "schema_version": "1.4.0", + "id": "GHSA-wfg4-322g-9vqv", + "summary": "memoffset allows reading uninitialized memory", + "details": "memoffset allows attempt of reading data from address `0` with arbitrary type. This behavior is an undefined behavior because address `0` to `std::mem::size_of\u003cT\u003e` may not have valid bit-pattern with `T`. Old implementation dereferences uninitialized memory obtained from `std::mem::align_of`. Older implementation prior to it allows using uninitialized data obtained from `std::mem::uninitialized` with arbitrary type then compute offset by taking the address of field-projection. This may also result in an undefined behavior for \"father\" that includes (directly or transitively) type that [does not allow to be uninitialized](https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html).\n\nThis flaw was corrected by using `std::ptr::addr_of` in \u003chttps://github.com/Gilnaa/memoffset/pull/50\u003e.\n", + "affected": [ + { + "package": { + "ecosystem": "crates.io", + "name": "memoffset", + "purl": "pkg:cargo/memoffset" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.6.2" + } + ] + } + ], + "database_specific": { + "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2023/06/GHSA-wfg4-322g-9vqv/GHSA-wfg4-322g-9vqv.json" + } + } + ], + "references": [ + { + "type": "WEB", + "url": "https://github.com/Gilnaa/memoffset/issues/24" + }, + { + "type": "WEB", + "url": "https://github.com/Gilnaa/memoffset/pull/50" + }, + { + "type": "PACKAGE", + "url": "https://github.com/Gilnaa/memoffset" + }, + { + "type": "WEB", + "url": "https://rustsec.org/advisories/RUSTSEC-2023-0045.html" + } + ], + "database_specific": { + "cwe_ids": [], + "github_reviewed": true, + "github_reviewed_at": "2023-06-21T22:06:29Z", + "nvd_published_at": null, + "severity": "MODERATE" + } + }, + { + "modified": "2023-07-08T12:30:19Z", + "published": "2023-06-21T12:00:00Z", + "schema_version": "1.4.0", + "id": "RUSTSEC-2023-0045", + "aliases": [ + "GHSA-wfg4-322g-9vqv" + ], + "summary": "memoffset allows reading uninitialized memory", + "details": "memoffset allows attempt of reading data from address `0` with arbitrary type. This behavior is an undefined behavior because address `0` to `std::mem::size_of\u003cT\u003e` may not have valid bit-pattern with `T`. Old implementation dereferences uninitialized memory obtained from `std::mem::align_of`. Older implementation prior to it allows using uninitialized data obtained from `std::mem::uninitialized` with arbitrary type then compute offset by taking the address of field-projection. This may also result in an undefined behavior for \"father\" that includes (directly or transitively) type that [does not allow to be uninitialized](https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html).\n\nThis flaw was corrected by using `std::ptr::addr_of` in \u003chttps://github.com/Gilnaa/memoffset/pull/50\u003e.", + "affected": [ + { + "package": { + "ecosystem": "crates.io", + "name": "memoffset", + "purl": "pkg:cargo/memoffset" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.6.2" + } + ] + } + ], + "database_specific": { + "categories": [ + "memory-corruption" + ], + "cvss": null, + "informational": "unsound", + "source": "https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2023-0045.json" + }, + "ecosystem_specific": { + "affects": { + "arch": [], + "functions": [ + "memoffset::offset_of" + ], + "os": [] + } + } + } + ], + "references": [ + { + "type": "PACKAGE", + "url": "https://crates.io/crates/memoffset" + }, + { + "type": "ADVISORY", + "url": "https://rustsec.org/advisories/RUSTSEC-2023-0045.html" + }, + { + "type": "REPORT", + "url": "https://github.com/Gilnaa/memoffset/issues/24" + } + ] + } + ], + "groups": [ + { + "ids": [ + "GHSA-wfg4-322g-9vqv", + "RUSTSEC-2023-0045" + ] + } + ] + }, + { + "package": { + "name": "smallvec", + "version": "1.6.0", + "ecosystem": "crates.io" + }, + "vulnerabilities": [ + { + "modified": "2023-06-13T20:51:42Z", + "published": "2022-05-24T17:40:21Z", + "schema_version": "1.4.0", + "id": "GHSA-43w2-9j62-hq99", + "aliases": [ + "CVE-2021-25900" + ], + "summary": "Buffer overflow in SmallVec::insert_many", + "details": "A bug in the SmallVec::insert_many method caused it to allocate a buffer that was smaller than needed. It then wrote past the end of the buffer, causing a buffer overflow and memory corruption on the heap. This bug was only triggered if the iterator passed to insert_many yielded more items than the lower bound returned from its size_hint method.\n\nThe flaw was corrected in smallvec 0.6.14 and 1.6.1, by ensuring that additional space is always reserved for each item inserted. The fix also simplified the implementation of insert_many to use less unsafe code, so it is easier to verify its correctness.", + "affected": [ + { + "package": { + "ecosystem": "crates.io", + "name": "smallvec", + "purl": "pkg:cargo/smallvec" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + { + "introduced": "0.6.3" + }, + { + "fixed": "0.6.14" + } + ] + } + ], + "database_specific": { + "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2022/05/GHSA-43w2-9j62-hq99/GHSA-43w2-9j62-hq99.json" + }, + "ecosystem_specific": { + "affected_functions": [ + "smallvec::SmallVec::insert_many" + ] + } + }, + { + "package": { + "ecosystem": "crates.io", + "name": "smallvec", + "purl": "pkg:cargo/smallvec" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + { + "introduced": "1.0.0" + }, + { + "fixed": "1.6.1" + } + ] + } + ], + "database_specific": { + "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2022/05/GHSA-43w2-9j62-hq99/GHSA-43w2-9j62-hq99.json" + }, + "ecosystem_specific": { + "affected_functions": [ + "smallvec::SmallVec::insert_many" + ] + } + } + ], + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "references": [ + { + "type": "ADVISORY", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-25900" + }, + { + "type": "WEB", + "url": "https://github.com/servo/rust-smallvec/issues/252" + }, + { + "type": "PACKAGE", + "url": "https://github.com/servo/rust-smallvec" + }, + { + "type": "WEB", + "url": "https://rustsec.org/advisories/RUSTSEC-2021-0003.html" + } + ], + "database_specific": { + "cwe_ids": [ + "CWE-787" + ], + "github_reviewed": true, + "github_reviewed_at": "2022-06-17T00:20:48Z", + "nvd_published_at": "2021-01-26T18:16:00Z", + "severity": "CRITICAL" + } + }, + { + "modified": "2023-06-13T13:10:24Z", + "published": "2021-01-08T12:00:00Z", + "schema_version": "1.4.0", + "id": "RUSTSEC-2021-0003", + "aliases": [ + "CVE-2021-25900", + "GHSA-43w2-9j62-hq99" + ], + "summary": "Buffer overflow in SmallVec::insert_many", + "details": "A bug in the `SmallVec::insert_many` method caused it to allocate a buffer that was smaller than needed. It then wrote past the end of the buffer, causing a buffer overflow and memory corruption on the heap.\n\nThis bug was only triggered if the iterator passed to `insert_many` yielded more items than the lower bound returned from its `size_hint` method.\n \nThe flaw was corrected in smallvec 0.6.14 and 1.6.1, by ensuring that additional space is always reserved for each item inserted. The fix also simplified the implementation of `insert_many` to use less unsafe code, so it is easier to verify its correctness.\n\nThank you to Yechan Bae (@Qwaz) and the Rust group at Georgia Tech’s SSLab for finding and reporting this bug.", + "affected": [ + { + "package": { + "ecosystem": "crates.io", + "name": "smallvec", + "purl": "pkg:cargo/smallvec" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + { + "introduced": "0.6.3" + }, + { + "fixed": "0.6.14" + }, + { + "introduced": "1.0.0" + }, + { + "fixed": "1.6.1" + } + ] + } + ], + "database_specific": { + "categories": [ + "memory-corruption" + ], + "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "informational": null, + "source": "https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2021-0003.json" + }, + "ecosystem_specific": { + "affects": { + "arch": [], + "functions": [ + "smallvec::SmallVec::insert_many" + ], + "os": [] + } + } + } + ], + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "references": [ + { + "type": "PACKAGE", + "url": "https://crates.io/crates/smallvec" + }, + { + "type": "ADVISORY", + "url": "https://rustsec.org/advisories/RUSTSEC-2021-0003.html" + }, + { + "type": "REPORT", + "url": "https://github.com/servo/rust-smallvec/issues/252" + } + ] + } + ], + "groups": [ + { + "ids": [ + "GHSA-43w2-9j62-hq99", + "RUSTSEC-2021-0003" + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/not-json.txt b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/not-json.txt new file mode 100644 index 0000000000..319318e4d7 --- /dev/null +++ b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/not-json.txt @@ -0,0 +1 @@ +this is not valid json! (I think) diff --git a/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/one-package-commit.json b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/one-package-commit.json new file mode 100644 index 0000000000..044efa3e48 --- /dev/null +++ b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/one-package-commit.json @@ -0,0 +1,19 @@ +{ + "results": [ + { + "source": { + "path": "/path/to/Gemfile.lock", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "commit": "9a6bd55c9d0722cb101fe85a3b22d89e4ff4fe52" + }, + "vulnerabilities": [], + "groups": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/one-package.json b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/one-package.json new file mode 100644 index 0000000000..ceeca26123 --- /dev/null +++ b/internal/lockfilescalibr/language/osv/osvscannerjson/testdata/one-package.json @@ -0,0 +1,21 @@ +{ + "results": [ + { + "source": { + "path": "/path/to/Gemfile.lock", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "activesupport", + "version": "7.0.7", + "ecosystem": "RubyGems" + }, + "vulnerabilities": [], + "groups": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/internal/lockfilescalibr/translation.go b/internal/lockfilescalibr/translation.go new file mode 100644 index 0000000000..5cebcbf6a9 --- /dev/null +++ b/internal/lockfilescalibr/translation.go @@ -0,0 +1,188 @@ +package lockfilescalibr + +import ( + "context" + "fmt" + "io/fs" + "os" + "sort" + + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem" + "github.com/google/osv-scalibr/extractor/filesystem/language/dart/pubspec" + "github.com/google/osv-scalibr/extractor/filesystem/language/dotnet/packageslockjson" + "github.com/google/osv-scalibr/extractor/filesystem/language/erlang/mixlock" + "github.com/google/osv-scalibr/extractor/filesystem/language/golang/gomod" + "github.com/google/osv-scalibr/extractor/filesystem/language/java/gradlelockfile" + "github.com/google/osv-scalibr/extractor/filesystem/language/java/gradleverificationmetadataxml" + "github.com/google/osv-scalibr/extractor/filesystem/language/java/pomxml" + "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagelockjson" + "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/pnpmlock" + "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/yarnlock" + "github.com/google/osv-scalibr/extractor/filesystem/language/php/composerlock" + "github.com/google/osv-scalibr/extractor/filesystem/language/python/pdmlock" + "github.com/google/osv-scalibr/extractor/filesystem/language/python/pipfilelock" + "github.com/google/osv-scalibr/extractor/filesystem/language/python/poetrylock" + "github.com/google/osv-scalibr/extractor/filesystem/language/python/requirements" + "github.com/google/osv-scalibr/extractor/filesystem/language/r/renvlock" + "github.com/google/osv-scalibr/extractor/filesystem/language/ruby/gemfilelock" + "github.com/google/osv-scalibr/extractor/filesystem/language/rust/cargolock" + + scalibrfs "github.com/google/osv-scalibr/fs" +) + +var lockfileExtractors = []filesystem.Extractor{ + // conanlock.Extractor{}, + packageslockjson.Extractor{}, + mixlock.Extractor{}, + pubspec.Extractor{}, + gomod.Extractor{}, + pomxml.Extractor{}, + gradlelockfile.Extractor{}, + gradleverificationmetadataxml.Extractor{}, + packagelockjson.Extractor{}, + pnpmlock.Extractor{}, + yarnlock.Extractor{}, + composerlock.Extractor{}, + pipfilelock.Extractor{}, + pdmlock.Extractor{}, + poetrylock.Extractor{}, + requirements.Extractor{}, + renvlock.Extractor{}, + gemfilelock.Extractor{}, + cargolock.Extractor{}, +} + +var lockfileExtractorMapping = map[string]string{ + "pubspec.lock": "dart/pubspec", + "pnpm-lock.yaml": "javascript/pnpmlock", + "yarn.lock": "javascript/yarnlock", + "package-lock.json": "javascript/packagelockjson", + "pom.xml": "java/pomxml", + "buildscript-gradle.lockfile": "java/gradlelockfile", + "gradle.lockfile": "java/gradlelockfile", + "verification-metadata.xml": "java/gradleverificationmetadataxml", + "poetry.lock": "python/poetrylock", + "Pipfile.lock": "python/Pipfilelock", + "pdm.lock": "python/pdmlock", + "requirements.txt": "python/requirements", + "Cargo.lock": "rust/Cargolock", + "composer.lock": "php/composerlock", + "mix.lock": "erlang/mixlock", + "renv.lock": "r/renvlock", + "packages.lock.json": "dotnet/packageslockjson", + // "conan.lock": "cpp/conanlock", + "go.mod": "go/gomod", + "Gemfile.lock": "ruby/gemfilelock", +} + +// ExtractWithExtractor attempts to extract the file at the given path with the extractor passed in +func ExtractWithExtractor(ctx context.Context, localPath string, ext filesystem.Extractor) ([]*extractor.Inventory, error) { + info, err := os.Stat(localPath) + if err != nil { + return nil, err + } + + return extractWithExtractor(ctx, localPath, info, ext) +} + +// Extract attempts to extract the file at the given path +// +// Args: +// - localPath: the path to the lockfile +// - extractAs: the name of the lockfile format to extract as (Using OSV-Scanner V1 extractor names) +// +// Returns: +// - []*extractor.Inventory: the extracted lockfile data +// - error: any errors encountered during extraction +// +// If extractAs is not specified, then the function will attempt to +// identify the lockfile format based on the file name. +// +// If no extractors are found, then ErrNoExtractorsFound is returned. +func Extract(ctx context.Context, localPath string, extractAs string) ([]*extractor.Inventory, error) { + info, err := os.Stat(localPath) + if err != nil { + return nil, err + } + + if extractAs != "" { + return extractAsSpecific(ctx, extractAs, localPath, info) + } + + output := []*extractor.Inventory{} + extractorFound := false + + for _, ext := range lockfileExtractors { + if ext.FileRequired(localPath, info) { + extractorFound = true + + inv, err := extractWithExtractor(ctx, localPath, info, ext) + if err != nil { + return nil, err + } + + output = append(output, inv...) + } + } + + if !extractorFound { + return nil, ErrNoExtractorsFound + } + + sort.Slice(output, func(i, j int) bool { + if output[i].Name == output[j].Name { + return output[i].Version < output[j].Version + } + + return output[i].Name < output[j].Name + }) + + return output, nil +} + +// Use the extractor specified by extractAs string key +func extractAsSpecific(ctx context.Context, extractAs string, localPath string, info fs.FileInfo) ([]*extractor.Inventory, error) { + for _, ext := range lockfileExtractors { + if lockfileExtractorMapping[extractAs] == ext.Name() { + return extractWithExtractor(ctx, localPath, info, ext) + } + } + + return nil, fmt.Errorf("%w, requested %s", ErrExtractorNotFound, extractAs) +} + +func extractWithExtractor(ctx context.Context, localPath string, info fs.FileInfo, ext filesystem.Extractor) ([]*extractor.Inventory, error) { + si, err := createScanInput(localPath, info) + if err != nil { + return nil, err + } + + inv, err := ext.Extract(ctx, si) + if err != nil { + return nil, fmt.Errorf("(extracting as %s) %w", ext.Name(), err) + } + + for i := range inv { + inv[i].Extractor = ext + } + + return inv, nil +} + +func createScanInput(path string, fileInfo fs.FileInfo) (*filesystem.ScanInput, error) { + reader, err := os.Open(path) + if err != nil { + return nil, err + } + + si := filesystem.ScanInput{ + FS: os.DirFS("/").(scalibrfs.FS), + Path: path, + Root: "/", + Reader: reader, + Info: fileInfo, + } + + return &si, nil +} diff --git a/internal/lockfilescalibr/translation_test.go b/internal/lockfilescalibr/translation_test.go new file mode 100644 index 0000000000..14c5f72e1d --- /dev/null +++ b/internal/lockfilescalibr/translation_test.go @@ -0,0 +1,23 @@ +package lockfilescalibr + +import ( + "testing" +) + +func TestLockfileScalibrMappingExists(t *testing.T) { + t.Parallel() + + for _, target := range lockfileExtractorMapping { + found := false + for _, ext := range lockfileExtractors { + if target == ext.Name() { + found = true + break + } + } + + if !found { + t.Errorf("Extractor %v not found.", target) + } + } +} diff --git a/internal/remediation/fixtures/santatracker/osv-scanner.toml b/internal/remediation/fixtures/santatracker/osv-scanner.toml index b399bb4c28..db94704b26 100644 --- a/internal/remediation/fixtures/santatracker/osv-scanner.toml +++ b/internal/remediation/fixtures/santatracker/osv-scanner.toml @@ -1,191 +1,4 @@ [[PackageOverrides]] -name = "@babel/traverse" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "@grpc/grpc-js" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "acorn" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "ajv" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "ansi-regex" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "braces" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "browserslist" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "dat.gui" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "get-func-name" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "glob-parent" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "google-closure-library" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "html-minifier" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "json-schema" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "json5" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "lodash" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "minimatch" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "minimist" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "node-fetch" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "node-forge " -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "node-forge" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "path-parse" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "pathval" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "postcss" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "protobufjs" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "qs" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "request" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "semver" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "terser" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "tough-cookie" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "ws" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "y18n" -ecosystem = "npm" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "yargs-parser" ecosystem = "npm" ignore = true reason = "This is an intentionally vulnerable test project" diff --git a/internal/remediation/fixtures/zeppelin-server/osv-scanner.toml b/internal/remediation/fixtures/zeppelin-server/osv-scanner.toml index 250f7b7530..d84c70b89e 100644 --- a/internal/remediation/fixtures/zeppelin-server/osv-scanner.toml +++ b/internal/remediation/fixtures/zeppelin-server/osv-scanner.toml @@ -1,143 +1,4 @@ [[PackageOverrides]] -name = "com.fasterxml.jackson.core:jackson-databind" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "com.google.guava:guava" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "com.jcraft:jsch" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "com.nimbusds:nimbus-jose-jwt" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "io.atomix:atomix" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "io.netty:netty-codec" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "io.netty:netty-handler" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.apache.commons:commons-compress" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.apache.commons:commons-configuration2" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.apache.directory.api:api-ldap-model" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.apache.mina:mina-core" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.apache.pdfbox:pdfbox" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.apache.shiro:shiro-core" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.apache.shiro:shiro-web" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.apache.thrift:libthrift" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.bouncycastle:bcprov-jdk15on" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.codehaus.jackson:jackson-mapper-asl" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.eclipse.jgit:org.eclipse.jgit" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.glassfish.jersey.core:jersey-common" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "com.google.code.gson:gson" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "commons-collections:commons-collections" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.apache.httpcomponents:httpclient" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.eclipse.jetty:jetty-webapp" -ecosystem = "Maven" -ignore = true -reason = "This is an intentionally vulnerable test project" - -[[PackageOverrides]] -name = "org.quartz-scheduler:quartz" ecosystem = "Maven" ignore = true reason = "This is an intentionally vulnerable test project" diff --git a/pkg/config/config.go b/pkg/config/config.go deleted file mode 100644 index 3e0058276a..0000000000 --- a/pkg/config/config.go +++ /dev/null @@ -1,270 +0,0 @@ -// Deprecated: this is now private and should not be used outside the scanner -package config - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "slices" - "strings" - "time" - - "github.com/BurntSushi/toml" - "github.com/google/osv-scanner/pkg/models" - "github.com/google/osv-scanner/pkg/reporter" -) - -const osvScannerConfigName = "osv-scanner.toml" - -// Ignore stuttering as that would be a breaking change -// TODO: V2 rename? -// -// Deprecated: this is now private and should not be used outside the scanner -// -//nolint:revive -type ConfigManager struct { - // Override to replace all other configs - OverrideConfig *Config - // Config to use if no config file is found alongside manifests - DefaultConfig Config - // Cache to store loaded configs - ConfigMap map[string]Config -} - -// Deprecated: this is now private and should not be used outside the scanner -type Config struct { - IgnoredVulns []IgnoreEntry `toml:"IgnoredVulns"` - PackageOverrides []PackageOverrideEntry `toml:"PackageOverrides"` - GoVersionOverride string `toml:"GoVersionOverride"` - // The path to config file that this config was loaded from, - // set by the scanner after having successfully parsed the file - LoadPath string `toml:"-"` -} - -// Deprecated: this is now private and should not be used outside the scanner -type IgnoreEntry struct { - ID string `toml:"id"` - IgnoreUntil time.Time `toml:"ignoreUntil"` - Reason string `toml:"reason"` -} - -// Deprecated: this is now private and should not be used outside the scanner -type PackageOverrideEntry struct { - Name string `toml:"name"` - // If the version is empty, the entry applies to all versions. - Version string `toml:"version"` - Ecosystem string `toml:"ecosystem"` - Group string `toml:"group"` - Ignore bool `toml:"ignore"` - Vulnerability Vulnerability `toml:"vulnerability"` - License License `toml:"license"` - EffectiveUntil time.Time `toml:"effectiveUntil"` - Reason string `toml:"reason"` -} - -func (e PackageOverrideEntry) matches(pkg models.PackageVulns) bool { - if e.Name != "" && e.Name != pkg.Package.Name { - return false - } - if e.Version != "" && e.Version != pkg.Package.Version { - return false - } - if e.Ecosystem != "" && e.Ecosystem != pkg.Package.Ecosystem { - return false - } - if e.Group != "" && !slices.Contains(pkg.DepGroups, e.Group) { - return false - } - - return true -} - -// Deprecated: this is now private and should not be used outside the scanner -type Vulnerability struct { - Ignore bool `toml:"ignore"` -} - -// Deprecated: this is now private and should not be used outside the scanner -type License struct { - Override []string `toml:"override"` - Ignore bool `toml:"ignore"` -} - -// Deprecated: this is now private and should not be used outside the scanner -func (c *Config) ShouldIgnore(vulnID string) (bool, IgnoreEntry) { - index := slices.IndexFunc(c.IgnoredVulns, func(e IgnoreEntry) bool { return e.ID == vulnID }) - if index == -1 { - return false, IgnoreEntry{} - } - ignoredLine := c.IgnoredVulns[index] - - return shouldIgnoreTimestamp(ignoredLine.IgnoreUntil), ignoredLine -} - -func (c *Config) filterPackageVersionEntries(pkg models.PackageVulns, condition func(PackageOverrideEntry) bool) (bool, PackageOverrideEntry) { - index := slices.IndexFunc(c.PackageOverrides, func(e PackageOverrideEntry) bool { - return e.matches(pkg) && condition(e) - }) - if index == -1 { - return false, PackageOverrideEntry{} - } - ignoredLine := c.PackageOverrides[index] - - return shouldIgnoreTimestamp(ignoredLine.EffectiveUntil), ignoredLine -} - -// ShouldIgnorePackage determines if the given package should be ignored based on override entries in the config -// -// Deprecated: this is now private and should not be used outside the scanner -func (c *Config) ShouldIgnorePackage(pkg models.PackageVulns) (bool, PackageOverrideEntry) { - return c.filterPackageVersionEntries(pkg, func(e PackageOverrideEntry) bool { - return e.Ignore - }) -} - -// Deprecated: Use ShouldIgnorePackage instead -func (c *Config) ShouldIgnorePackageVersion(name, version, ecosystem string) (bool, PackageOverrideEntry) { - return c.ShouldIgnorePackage(models.PackageVulns{ - Package: models.PackageInfo{ - Name: name, - Version: version, - Ecosystem: ecosystem, - }, - }) -} - -// ShouldIgnorePackageVulnerabilities determines if the given package should have its vulnerabilities ignored based on override entries in the config -// -// Deprecated: this is now private and should not be used outside the scanner -func (c *Config) ShouldIgnorePackageVulnerabilities(pkg models.PackageVulns) bool { - overrides, _ := c.filterPackageVersionEntries(pkg, func(e PackageOverrideEntry) bool { - return e.Vulnerability.Ignore - }) - - return overrides -} - -// ShouldOverridePackageLicense determines if the given package should have its license ignored or changed based on override entries in the config -// -// Deprecated: this is now private and should not be used outside the scanner -func (c *Config) ShouldOverridePackageLicense(pkg models.PackageVulns) (bool, PackageOverrideEntry) { - return c.filterPackageVersionEntries(pkg, func(e PackageOverrideEntry) bool { - return e.License.Ignore || len(e.License.Override) > 0 - }) -} - -// Deprecated: Use ShouldOverridePackageLicense instead -func (c *Config) ShouldOverridePackageVersionLicense(name, version, ecosystem string) (bool, PackageOverrideEntry) { - return c.ShouldOverridePackageLicense(models.PackageVulns{ - Package: models.PackageInfo{ - Name: name, - Version: version, - Ecosystem: ecosystem, - }, - }) -} - -func shouldIgnoreTimestamp(ignoreUntil time.Time) bool { - if ignoreUntil.IsZero() { - // If IgnoreUntil is not set, should ignore. - return true - } - // Should ignore if IgnoreUntil is still after current time - // Takes timezone offsets into account if it is specified. otherwise it's using local time - return ignoreUntil.After(time.Now()) -} - -// Sets the override config by reading the config file at configPath. -// Will return an error if loading the config file fails -// -// Deprecated: this is now private and should not be used outside the scanner -func (c *ConfigManager) UseOverride(configPath string) error { - config, configErr := tryLoadConfig(configPath) - if configErr != nil { - return configErr - } - c.OverrideConfig = &config - - return nil -} - -// Attempts to get the config -// -// Deprecated: this is now private and should not be used outside the scanner -func (c *ConfigManager) Get(r reporter.Reporter, targetPath string) Config { - if c.OverrideConfig != nil { - return *c.OverrideConfig - } - - configPath, err := normalizeConfigLoadPath(targetPath) - 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.PrintErrorf("Can't find config path: %s\n", err) - return Config{} - } - - config, alreadyExists := c.ConfigMap[configPath] - if alreadyExists { - return config - } - - config, configErr := tryLoadConfig(configPath) - if configErr == nil { - r.Infof("Loaded filter from: %s\n", config.LoadPath) - } else { - // anything other than the config file not existing is most likely due to an invalid config file - if !errors.Is(configErr, os.ErrNotExist) { - r.Errorf("Ignored invalid config file at: %s\n", configPath) - r.Verbosef("Config file %s is invalid because: %v\n", configPath, configErr) - } - // If config doesn't exist, use the default config - config = c.DefaultConfig - } - c.ConfigMap[configPath] = config - - return config -} - -// Finds the containing folder of `target`, then appends osvScannerConfigName -func normalizeConfigLoadPath(target string) (string, error) { - stat, err := os.Stat(target) - if err != nil { - return "", fmt.Errorf("failed to stat target: %w", err) - } - - var containingFolder string - if !stat.IsDir() { - containingFolder = filepath.Dir(target) - } else { - containingFolder = target - } - configPath := filepath.Join(containingFolder, osvScannerConfigName) - - return configPath, nil -} - -// tryLoadConfig attempts to parse the config file at the given path as TOML, -// returning the Config object if successful or otherwise the error -func tryLoadConfig(configPath string) (Config, error) { - config := Config{} - m, err := toml.DecodeFile(configPath, &config) - if err == nil { - unknownKeys := m.Undecoded() - - if len(unknownKeys) > 0 { - keys := make([]string, 0, len(unknownKeys)) - - for _, key := range unknownKeys { - keys = append(keys, key.String()) - } - - return Config{}, fmt.Errorf("unknown keys in config file: %s", strings.Join(keys, ", ")) - } - - config.LoadPath = configPath - } - - return config, err -} diff --git a/pkg/config/config_internal_test.go b/pkg/config/config_internal_test.go deleted file mode 100644 index 2336c2ae23..0000000000 --- a/pkg/config/config_internal_test.go +++ /dev/null @@ -1,1320 +0,0 @@ -package config - -import ( - "fmt" - "reflect" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/osv-scanner/pkg/models" -) - -// Attempts to normalize any file paths in the given `output` so that they can -// be compared reliably regardless of the file path separator being used. -// -// Namely, escaped forward slashes are replaced with backslashes. -func normalizeFilePaths(t *testing.T, output string) string { - t.Helper() - - return strings.ReplaceAll(strings.ReplaceAll(output, "\\\\", "/"), "\\", "/") -} - -func Test_normalizeConfigLoadPath(t *testing.T) { - t.Parallel() - - type args struct { - target string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "target does not exist", - args: args{ - target: "./fixtures/testdatainner/does-not-exist", - }, - want: "", - wantErr: true, - }, - { - name: "target is file in directory", - args: args{ - target: "./fixtures/testdatainner/innerFolder/test.yaml", - }, - want: "fixtures/testdatainner/innerFolder/osv-scanner.toml", - wantErr: false, - }, - { - name: "target is inner directory with trailing slash", - args: args{ - target: "./fixtures/testdatainner/innerFolder/", - }, - want: "fixtures/testdatainner/innerFolder/osv-scanner.toml", - wantErr: false, - }, - { - name: "target is inner directory without trailing slash", - args: args{ - target: "./fixtures/testdatainner/innerFolder", - }, - want: "fixtures/testdatainner/innerFolder/osv-scanner.toml", - wantErr: false, - }, - { - name: "target is directory with trailing slash", - args: args{ - target: "./fixtures/testdatainner/", - }, - want: "fixtures/testdatainner/osv-scanner.toml", - wantErr: false, - }, - { - name: "target is file in directory", - args: args{ - target: "./fixtures/testdatainner/some-manifest.yaml", - }, - want: "fixtures/testdatainner/osv-scanner.toml", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got, err := normalizeConfigLoadPath(tt.args.target) - if (err != nil) != tt.wantErr { - t.Errorf("normalizeConfigLoadPath() error = %v, wantErr %v", err, tt.wantErr) - return - } - - got = normalizeFilePaths(t, got) - if got != tt.want { - t.Errorf("normalizeConfigLoadPath() got = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_tryLoadConfig(t *testing.T) { - t.Parallel() - - type args struct { - configPath string - } - tests := []struct { - name string - args args - want Config - wantErr bool - }{ - { - name: "config does not exist", - args: args{ - configPath: "./fixtures/testdatainner/does-not-exist", - }, - want: Config{}, - wantErr: true, - }, - { - name: "config has some ignored vulnerabilities and package overrides", - args: args{ - configPath: "./fixtures/testdatainner/osv-scanner.toml", - }, - want: Config{ - LoadPath: "./fixtures/testdatainner/osv-scanner.toml", - IgnoredVulns: []IgnoreEntry{ - { - ID: "GO-2022-0968", - }, - { - ID: "GO-2022-1059", - }, - }, - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib", - Version: "1.0.0", - Ecosystem: "Go", - Ignore: true, - Reason: "abc", - }, - { - Name: "my-pkg", - Version: "1.0.0", - Ecosystem: "Go", - Reason: "abc", - Ignore: true, - License: License{ - Override: []string{"MIT", "0BSD"}, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "load path cannot be overridden via config", - args: args{ - configPath: "./fixtures/testdatainner/osv-scanner-load-path.toml", - }, - want: Config{ - LoadPath: "", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got, err := tryLoadConfig(tt.args.configPath) - if (err != nil) != tt.wantErr { - t.Errorf("tryLoadConfig() error = %v, wantErr %v", err, tt.wantErr) - return - } - if diff := cmp.Diff(tt.want, got); diff != "" { - t.Errorf("tryLoadConfig() mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestTryLoadConfig_UnknownKeys(t *testing.T) { - t.Parallel() - - tests := []struct { - configPath string - unknownMsg string - }{ - { - configPath: "./fixtures/unknown-key-1.toml", - unknownMsg: "IgnoredVulns.ignoreUntilTime", - }, - { - configPath: "./fixtures/unknown-key-2.toml", - unknownMsg: "IgnoredVulns.ignoreUntiI", - }, - { - configPath: "./fixtures/unknown-key-3.toml", - unknownMsg: "IgnoredVulns.reasoning", - }, - { - configPath: "./fixtures/unknown-key-4.toml", - unknownMsg: "PackageOverrides.skip", - }, - { - configPath: "./fixtures/unknown-key-5.toml", - unknownMsg: "PackageOverrides.license.skip", - }, - { - configPath: "./fixtures/unknown-key-6.toml", - unknownMsg: "RustVersionOverride", - }, - { - configPath: "./fixtures/unknown-key-7.toml", - unknownMsg: "RustVersionOverride, PackageOverrides.skip", - }, - } - - for _, testData := range tests { - c, err := tryLoadConfig(testData.configPath) - - // we should always be returning an empty config on error - if diff := cmp.Diff(Config{}, c); diff != "" { - t.Errorf("tryLoadConfig() mismatch (-want +got):\n%s", diff) - } - if err == nil { - t.Fatal("tryLoadConfig() did not return an error") - } - - wantMsg := fmt.Sprintf("unknown keys in config file: %v", testData.unknownMsg) - - if err.Error() != wantMsg { - t.Errorf("tryLoadConfig() error = '%v', want '%s'", err, wantMsg) - } - } -} - -func TestConfig_ShouldIgnore(t *testing.T) { - t.Parallel() - - type args struct { - vulnID string - } - tests := []struct { - name string - config Config - args args - wantOk bool - wantEntry IgnoreEntry - }{ - // entry exists - { - name: "", - config: Config{ - IgnoredVulns: []IgnoreEntry{ - { - ID: "GHSA-123", - IgnoreUntil: time.Time{}, - Reason: "", - }, - }, - }, - args: args{ - vulnID: "GHSA-123", - }, - wantOk: true, - wantEntry: IgnoreEntry{ - ID: "GHSA-123", - IgnoreUntil: time.Time{}, - Reason: "", - }, - }, - // entry does not exist - { - name: "", - config: Config{ - IgnoredVulns: []IgnoreEntry{ - { - ID: "GHSA-123", - IgnoreUntil: time.Time{}, - Reason: "", - }, - }, - }, - args: args{ - vulnID: "nonexistent", - }, - wantOk: false, - wantEntry: IgnoreEntry{}, - }, - // ignored until a time in the past - { - name: "", - config: Config{ - IgnoredVulns: []IgnoreEntry{ - { - ID: "GHSA-123", - IgnoreUntil: time.Now().Add(-time.Hour).Round(time.Second), - Reason: "", - }, - }, - }, - args: args{ - vulnID: "GHSA-123", - }, - wantOk: false, - wantEntry: IgnoreEntry{ - ID: "GHSA-123", - IgnoreUntil: time.Now().Add(-time.Hour).Round(time.Second), - Reason: "", - }, - }, - // ignored until a time in the future - { - name: "", - config: Config{ - IgnoredVulns: []IgnoreEntry{ - { - ID: "GHSA-123", - IgnoreUntil: time.Now().Add(time.Hour).Round(time.Second), - Reason: "", - }, - }, - }, - args: args{ - vulnID: "GHSA-123", - }, - wantOk: true, - wantEntry: IgnoreEntry{ - ID: "GHSA-123", - IgnoreUntil: time.Now().Add(time.Hour).Round(time.Second), - Reason: "", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - gotOk, gotEntry := tt.config.ShouldIgnore(tt.args.vulnID) - if gotOk != tt.wantOk { - t.Errorf("ShouldIgnore() gotOk = %v, wantOk %v", gotOk, tt.wantOk) - } - if !reflect.DeepEqual(gotEntry, tt.wantEntry) { - t.Errorf("ShouldIgnore() gotEntry = %v, wantEntry %v", gotEntry, tt.wantEntry) - } - }) - } -} - -func TestConfig_ShouldIgnorePackage(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - config Config - args models.PackageVulns - wantOk bool - wantEntry PackageOverrideEntry - }{ - { - name: "Everything-level entry exists", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - DepGroups: []string{"dev"}, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - // ------------------------------------------------------------------------- - { - name: "Ecosystem-level entry exists and does match", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - DepGroups: []string{"dev"}, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - { - name: "Ecosystem-level entry exists and does not match", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib2", - Version: "1.0.0", - Ecosystem: "npm", - }, - DepGroups: []string{"dev"}, - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - // ------------------------------------------------------------------------- - { - name: "Group-level entry exists and does match", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Group: "dev", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - DepGroups: []string{"dev"}, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Group: "dev", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - { - name: "Group-level entry exists and does not match", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Group: "dev", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib2", - Version: "1.0.0", - Ecosystem: "npm", - }, - DepGroups: []string{"optional"}, - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - { - name: "Group-level entry exists and does not match when empty", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Group: "dev", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib2", - Version: "1.0.0", - Ecosystem: "npm", - }, - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - // ------------------------------------------------------------------------- - { - name: "Version-level entry exists and does match", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Version: "1.0.0", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - DepGroups: []string{"dev"}, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Version: "1.0.0", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - { - name: "Version-level entry exists and does not match", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Version: "1.0.0", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.1", - Ecosystem: "Go", - }, - DepGroups: []string{"dev"}, - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - // ------------------------------------------------------------------------- - { - name: "Name-level entry exists and does match", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - DepGroups: []string{"dev"}, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - { - name: "Name-level entry exists and does not match", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib2", - Version: "1.0.0", - Ecosystem: "npm", - }, - DepGroups: []string{"dev"}, - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - // ------------------------------------------------------------------------- - { - name: "Name, Version, and Ecosystem entry exists", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - { - name: "Name and Ecosystem entry exists", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - { - name: "Name, Ecosystem, and Group entry exists and matches", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ecosystem: "Go", - Group: "dev", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - DepGroups: []string{"dev"}, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Ecosystem: "Go", - Group: "dev", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - { - name: "Name, Ecosystem, and Group entry exists but does not match", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ecosystem: "Go", - Group: "dev", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - DepGroups: []string{"prod"}, - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - { - name: "Entry doesn't exist", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "2.0.0", - Ecosystem: "Go", - Ignore: false, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - { - Name: "lib2", - Version: "2.0.0", - Ignore: true, - Ecosystem: "Go", - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "2.0.0", - Ecosystem: "Go", - }, - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - gotOk, gotEntry := tt.config.ShouldIgnorePackage(tt.args) - if gotOk != tt.wantOk { - t.Errorf("ShouldIgnorePackage() gotOk = %v, wantOk %v", gotOk, tt.wantOk) - } - if !reflect.DeepEqual(gotEntry, tt.wantEntry) { - t.Errorf("ShouldIgnorePackage() gotEntry = %v, wantEntry %v", gotEntry, tt.wantEntry) - } - }) - } -} - -func TestConfig_ShouldIgnorePackageVersion(t *testing.T) { - t.Parallel() - - type args struct { - name string - version string - ecosystem string - } - tests := []struct { - name string - config Config - args args - wantOk bool - wantEntry PackageOverrideEntry - }{ - { - name: "Version-level entry exists", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: args{ - name: "lib1", - version: "1.0.0", - ecosystem: "Go", - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - { - name: "Package-level entry exists", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: args{ - name: "lib1", - version: "1.0.0", - ecosystem: "Go", - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Ecosystem: "Go", - Ignore: true, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - { - name: "Entry doesn't exist", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "2.0.0", - Ecosystem: "Go", - Ignore: false, - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - { - Name: "lib2", - Version: "2.0.0", - Ignore: true, - Ecosystem: "Go", - EffectiveUntil: time.Time{}, - Reason: "abc", - }, - }, - }, - args: args{ - name: "lib1", - version: "2.0.0", - ecosystem: "Go", - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - gotOk, gotEntry := tt.config.ShouldIgnorePackageVersion(tt.args.name, tt.args.version, tt.args.ecosystem) - if gotOk != tt.wantOk { - t.Errorf("ShouldIgnorePackageVersion() gotOk = %v, wantOk %v", gotOk, tt.wantOk) - } - if !reflect.DeepEqual(gotEntry, tt.wantEntry) { - t.Errorf("ShouldIgnorePackageVersion() gotEntry = %v, wantEntry %v", gotEntry, tt.wantEntry) - } - }) - } -} - -func TestConfig_ShouldIgnorePackageVulnerabilities(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - config Config - args models.PackageVulns - wantOk bool - }{ - { - name: "Exact version entry exists with ignore", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - Vulnerability: Vulnerability{ - Ignore: true, - }, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - }, - wantOk: true, - }, - { - name: "Version entry doesn't exist with ignore", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - Vulnerability: Vulnerability{ - Ignore: true, - }, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.1", - Ecosystem: "Go", - }, - }, - wantOk: false, - }, - { - name: "Name matches with ignore", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ecosystem: "Go", - Vulnerability: Vulnerability{ - Ignore: true, - }, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.1", - Ecosystem: "Go", - }, - }, - wantOk: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - gotOk := tt.config.ShouldIgnorePackageVulnerabilities(tt.args) - if gotOk != tt.wantOk { - t.Errorf("ShouldIgnorePackageVulnerabilities() gotOk = %v, wantOk %v", gotOk, tt.wantOk) - } - }) - } -} - -func TestConfig_ShouldOverridePackageLicense(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - config Config - args models.PackageVulns - wantOk bool - wantEntry PackageOverrideEntry - }{ - { - name: "Exact version entry exists with override", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - { - name: "Exact version entry exists with ignore", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - License: License{ - Ignore: true, - }, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - }, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - License: License{ - Ignore: true, - }, - Reason: "abc", - }, - }, - { - name: "Version entry doesn't exist with override", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.1", - Ecosystem: "Go", - }, - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - { - name: "Version entry doesn't exist with ignore", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - License: License{ - Ignore: true, - }, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.1", - Ecosystem: "Go", - }, - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - { - name: "Name matches with override", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.1", - Ecosystem: "Go", - }, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - { - name: "Name matches with ignore", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ecosystem: "Go", - License: License{ - Ignore: true, - }, - Reason: "abc", - }, - }, - }, - args: models.PackageVulns{ - Package: models.PackageInfo{ - Name: "lib1", - Version: "1.0.1", - Ecosystem: "Go", - }, - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Ecosystem: "Go", - License: License{ - Ignore: true, - }, - Reason: "abc", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - gotOk, gotEntry := tt.config.ShouldOverridePackageLicense(tt.args) - if gotOk != tt.wantOk { - t.Errorf("ShouldOverridePackageLicense() gotOk = %v, wantOk %v", gotOk, tt.wantOk) - } - if !reflect.DeepEqual(gotEntry, tt.wantEntry) { - t.Errorf("ShouldOverridePackageLicense() gotEntry = %v, wantEntry %v", gotEntry, tt.wantEntry) - } - }) - } -} - -func TestConfig_ShouldOverridePackageVersionLicense(t *testing.T) { - t.Parallel() - - type args struct { - name string - version string - ecosystem string - } - tests := []struct { - name string - config Config - args args - wantOk bool - wantEntry PackageOverrideEntry - }{ - { - name: "Exact version entry exists", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - }, - args: args{ - name: "lib1", - version: "1.0.0", - ecosystem: "Go", - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - { - name: "Version entry doesn't exist", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Version: "1.0.0", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - }, - args: args{ - name: "lib1", - version: "1.0.1", - ecosystem: "Go", - }, - wantOk: false, - wantEntry: PackageOverrideEntry{}, - }, - { - name: "Name matches", - config: Config{ - PackageOverrides: []PackageOverrideEntry{ - { - Name: "lib1", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - }, - args: args{ - name: "lib1", - version: "1.0.1", - ecosystem: "Go", - }, - wantOk: true, - wantEntry: PackageOverrideEntry{ - Name: "lib1", - Ecosystem: "Go", - License: License{ - Override: []string{"mit"}, - }, - Reason: "abc", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - gotOk, gotEntry := tt.config.ShouldOverridePackageVersionLicense(tt.args.name, tt.args.version, tt.args.ecosystem) - if gotOk != tt.wantOk { - t.Errorf("ShouldOverridePackageVersionLicense() gotOk = %v, wantOk %v", gotOk, tt.wantOk) - } - if !reflect.DeepEqual(gotEntry, tt.wantEntry) { - t.Errorf("ShouldOverridePackageVersionLicense() gotEntry = %v, wantEntry %v", gotEntry, tt.wantEntry) - } - }) - } -} diff --git a/pkg/config/fixtures/testdatainner/innerFolder/test.yaml b/pkg/config/fixtures/testdatainner/innerFolder/test.yaml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pkg/config/fixtures/testdatainner/osv-scanner-load-path.toml b/pkg/config/fixtures/testdatainner/osv-scanner-load-path.toml deleted file mode 100644 index 001548b76d..0000000000 --- a/pkg/config/fixtures/testdatainner/osv-scanner-load-path.toml +++ /dev/null @@ -1 +0,0 @@ -LoadPath = "a/b/c" diff --git a/pkg/config/fixtures/testdatainner/osv-scanner.toml b/pkg/config/fixtures/testdatainner/osv-scanner.toml deleted file mode 100644 index f9be2c0f2e..0000000000 --- a/pkg/config/fixtures/testdatainner/osv-scanner.toml +++ /dev/null @@ -1,25 +0,0 @@ -[[IgnoredVulns]] -id = "GO-2022-0968" -# ignoreUntil = 2022-11-09 -# reason = "" # Optional reason - -[[IgnoredVulns]] -id = "GO-2022-1059" -# ignoreUntil = 2022-11-09 # Optional exception expiry date -# reason = "" # Optional reason - -[[PackageOverrides]] -name = "lib" -version = "1.0.0" -ecosystem = "Go" -ignore = true -# effectiveUntil = 2022-11-09 # Optional exception expiry date -reason = "abc" - -[[PackageOverrides]] -name = "my-pkg" -version = "1.0.0" -ecosystem = "Go" -ignore = true -reason = "abc" -license.override = ["MIT", "0BSD"] diff --git a/pkg/config/fixtures/testdatainner/some-manifest.yaml b/pkg/config/fixtures/testdatainner/some-manifest.yaml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pkg/config/fixtures/unknown-key-1.toml b/pkg/config/fixtures/unknown-key-1.toml deleted file mode 100644 index 2c8538325b..0000000000 --- a/pkg/config/fixtures/unknown-key-1.toml +++ /dev/null @@ -1,4 +0,0 @@ -[[IgnoredVulns]] -id = "GHSA-jgvc-jfgh-rjvv" -ignoreUntilTime = 2024-08-02 # whoops, should be "ignoreUntil" -reason = "..." diff --git a/pkg/config/fixtures/unknown-key-2.toml b/pkg/config/fixtures/unknown-key-2.toml deleted file mode 100644 index 7b6d964f43..0000000000 --- a/pkg/config/fixtures/unknown-key-2.toml +++ /dev/null @@ -1,4 +0,0 @@ -[[IgnoredVulns]] -id = "GHSA-jgvc-jfgh-rjvv" -ignoreUntiI = 2024-08-02 # whoops, should be "ignoreUntil" -reason = "..." diff --git a/pkg/config/fixtures/unknown-key-3.toml b/pkg/config/fixtures/unknown-key-3.toml deleted file mode 100644 index bce7ed9a19..0000000000 --- a/pkg/config/fixtures/unknown-key-3.toml +++ /dev/null @@ -1,4 +0,0 @@ -[[IgnoredVulns]] -id = "GHSA-jgvc-jfgh-rjvv" -ignoreUntil = 2024-08-02 -reasoning = "..." # whoops, should be "reason" diff --git a/pkg/config/fixtures/unknown-key-4.toml b/pkg/config/fixtures/unknown-key-4.toml deleted file mode 100644 index f508c89dd1..0000000000 --- a/pkg/config/fixtures/unknown-key-4.toml +++ /dev/null @@ -1,4 +0,0 @@ -[[PackageOverrides]] -ecosystem = "npm" -skip = true # whoops, should be "ignore" -license.override = ["0BSD"] diff --git a/pkg/config/fixtures/unknown-key-5.toml b/pkg/config/fixtures/unknown-key-5.toml deleted file mode 100644 index d1d832aed0..0000000000 --- a/pkg/config/fixtures/unknown-key-5.toml +++ /dev/null @@ -1,3 +0,0 @@ -[[PackageOverrides]] -ecosystem = "npm" -license.skip = false # whoops, should be "license.ignore" diff --git a/pkg/config/fixtures/unknown-key-6.toml b/pkg/config/fixtures/unknown-key-6.toml deleted file mode 100644 index 80f0b87eee..0000000000 --- a/pkg/config/fixtures/unknown-key-6.toml +++ /dev/null @@ -1 +0,0 @@ -RustVersionOverride = "1.2.3" # whoops, not supported diff --git a/pkg/config/fixtures/unknown-key-7.toml b/pkg/config/fixtures/unknown-key-7.toml deleted file mode 100644 index 044156ccec..0000000000 --- a/pkg/config/fixtures/unknown-key-7.toml +++ /dev/null @@ -1,5 +0,0 @@ -RustVersionOverride = "1.2.3" # whoops, not supported - -[[PackageOverrides]] -ecosystem = "npm" -skip = true # whoops, should be "ignore" diff --git a/pkg/depsdev/license.go b/pkg/depsdev/license.go deleted file mode 100644 index 67fc3398e8..0000000000 --- a/pkg/depsdev/license.go +++ /dev/null @@ -1,125 +0,0 @@ -// Deprecated: this is now private and should not be used outside the scanner -package depsdev - -import ( - "context" - "crypto/x509" - "fmt" - - "github.com/google/osv-scanner/pkg/lockfile" - "github.com/google/osv-scanner/pkg/models" - "github.com/google/osv-scanner/pkg/osv" - - depsdevpb "deps.dev/api/v3" - "golang.org/x/sync/errgroup" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/status" -) - -// DepsdevAPI is the URL to the deps.dev API. It is documented at -// docs.deps.dev/api. -// -// Deprecated: this is now private and should not be used outside the scanner -const DepsdevAPI = "api.deps.dev:443" - -// System maps from a lockfile system to the depsdev API system. -// -// Deprecated: this is now private and should not be used outside the scanner -var System = map[lockfile.Ecosystem]depsdevpb.System{ - lockfile.NpmEcosystem: depsdevpb.System_NPM, - lockfile.NuGetEcosystem: depsdevpb.System_NUGET, - lockfile.CargoEcosystem: depsdevpb.System_CARGO, - lockfile.GoEcosystem: depsdevpb.System_GO, - lockfile.MavenEcosystem: depsdevpb.System_MAVEN, - lockfile.PipEcosystem: depsdevpb.System_PYPI, -} - -// VersionQuery constructs a GetVersion request from the arguments. -// -// Deprecated: this is now private and should not be used outside the scanner -func VersionQuery(system depsdevpb.System, name string, version string) *depsdevpb.GetVersionRequest { - if system == depsdevpb.System_GO { - version = "v" + version - } - - return &depsdevpb.GetVersionRequest{ - VersionKey: &depsdevpb.VersionKey{ - System: system, - Name: name, - Version: version, - }, - } -} - -// MakeVersionRequests wraps MakeVersionRequestsWithContext using context.Background. -// -// Deprecated: this is now private and should not be used outside the scanner -func MakeVersionRequests(queries []*depsdevpb.GetVersionRequest) ([][]models.License, error) { - return MakeVersionRequestsWithContext(context.Background(), queries) -} - -// MakeVersionRequestsWithContext calls the deps.dev GetVersion gRPC API endpoint for each -// query. It makes these requests concurrently, sharing the single HTTP/2 -// connection. The order in which the requests are specified should correspond -// to the order of licenses returned by this function. -// -// Deprecated: this is now private and should not be used outside the scanner -func MakeVersionRequestsWithContext(ctx context.Context, queries []*depsdevpb.GetVersionRequest) ([][]models.License, error) { - certPool, err := x509.SystemCertPool() - if err != nil { - return nil, fmt.Errorf("getting system cert pool: %w", err) - } - creds := credentials.NewClientTLSFromCert(certPool, "") - dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(creds)} - - if osv.RequestUserAgent != "" { - dialOpts = append(dialOpts, grpc.WithUserAgent(osv.RequestUserAgent)) - } - - conn, err := grpc.NewClient(DepsdevAPI, dialOpts...) - if err != nil { - return nil, fmt.Errorf("dialing deps.dev gRPC API: %w", err) - } - client := depsdevpb.NewInsightsClient(conn) - - licenses := make([][]models.License, len(queries)) - g, ctx := errgroup.WithContext(ctx) - for i := range queries { - if queries[i] == nil { - // This may be a private package. - licenses[i] = []models.License{models.License("UNKNOWN")} - continue - } - g.Go(func() error { - resp, err := client.GetVersion(ctx, queries[i]) - if err != nil { - if status.Code(err) == codes.NotFound { - licenses[i] = append(licenses[i], "UNKNOWN") - return nil - } - - return err - } - ls := make([]models.License, len(resp.GetLicenses())) - for j, license := range resp.GetLicenses() { - ls[j] = models.License(license) - } - if len(ls) == 0 { - // The deps.dev API will return an - // empty slice if the license is - // unknown. - ls = []models.License{models.License("UNKNOWN")} - } - licenses[i] = ls - - return nil - }) - } - if err := g.Wait(); err != nil { - return nil, err - } - - return licenses, nil -} diff --git a/pkg/grouper/grouper.go b/pkg/grouper/grouper.go deleted file mode 100644 index c64399915a..0000000000 --- a/pkg/grouper/grouper.go +++ /dev/null @@ -1,76 +0,0 @@ -// Deprecated: this is now private and should not be used outside the scanner -package grouper - -import ( - "slices" - "sort" - - "golang.org/x/exp/maps" - - "github.com/google/osv-scanner/internal/identifiers" - "github.com/google/osv-scanner/pkg/models" -) - -func hasAliasIntersection(v1, v2 IDAliases) bool { - // Check if any aliases intersect. - for _, alias := range v1.Aliases { - if slices.Contains(v2.Aliases, alias) { - return true - } - } - // Check if either IDs are in the others' aliases. - return slices.Contains(v1.Aliases, v2.ID) || slices.Contains(v2.Aliases, v1.ID) -} - -// Group groups vulnerabilities by aliases. -// -// Deprecated: this is now private and should not be used outside the scanner -func Group(vulns []IDAliases) []models.GroupInfo { - // Mapping of `vulns` index to a group ID. A group ID is just another index in the `vulns` slice. - groups := make([]int, len(vulns)) - - // Initially make every vulnerability its own group. - for i := range vulns { - groups[i] = i - } - - // Do a pair-wise (n^2) comparison and merge all intersecting vulns. - for i := range vulns { - for j := i + 1; j < len(vulns); j++ { - if hasAliasIntersection(vulns[i], vulns[j]) { - // Merge the two groups. Use the smaller index as the representative ID. - groups[i] = min(groups[i], groups[j]) - groups[j] = groups[i] - } - } - } - - // Extract groups into the final result structure. - extractedGroups := map[int][]string{} - extractedAliases := map[int][]string{} - for i, gid := range groups { - extractedGroups[gid] = append(extractedGroups[gid], vulns[i].ID) - extractedAliases[gid] = append(extractedAliases[gid], vulns[i].Aliases...) - } - - // Sort by group ID to maintain stable order for tests. - sortedKeys := maps.Keys(extractedGroups) - sort.Ints(sortedKeys) - - result := make([]models.GroupInfo, 0, len(sortedKeys)) - for _, key := range sortedKeys { - // Sort the strings so they are always in the same order - slices.SortFunc(extractedGroups[key], identifiers.IDSortFunc) - - // Add IDs to aliases - extractedAliases[key] = append(extractedAliases[key], extractedGroups[key]...) - - // Dedup entries - sort.Strings(extractedAliases[key]) - extractedAliases[key] = slices.Compact(extractedAliases[key]) - - result = append(result, models.GroupInfo{IDs: extractedGroups[key], Aliases: extractedAliases[key]}) - } - - return result -} diff --git a/pkg/grouper/grouper_models.go b/pkg/grouper/grouper_models.go deleted file mode 100644 index 1b759e74e3..0000000000 --- a/pkg/grouper/grouper_models.go +++ /dev/null @@ -1,36 +0,0 @@ -// Deprecated: this is now private and should not be used outside the scanner -package grouper - -import ( - "strings" - - "github.com/google/osv-scanner/pkg/models" -) - -// Deprecated: this is now private and should not be used outside the scanner -type IDAliases struct { - ID string - Aliases []string -} - -// Deprecated: this is now private and should not be used outside the scanner -func ConvertVulnerabilityToIDAliases(c []models.Vulnerability) []IDAliases { - output := []IDAliases{} - for _, v := range c { - idAliases := IDAliases{ - ID: v.ID, - Aliases: v.Aliases, - } - - // For Debian Security Advisory data, - // all related CVEs should be bundled together, as they are part of this DSA. - // TODO(gongh@): Revisit and provide a universal way to handle all Linux distro advisories. - if strings.Split(v.ID, "-")[0] == "DSA" { - idAliases.Aliases = append(idAliases.Aliases, v.Related...) - } - - output = append(output, idAliases) - } - - return output -} diff --git a/pkg/grouper/grouper_test.go b/pkg/grouper/grouper_test.go deleted file mode 100644 index 596646f4fc..0000000000 --- a/pkg/grouper/grouper_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package grouper_test - -import ( - "testing" - - "github.com/google/osv-scanner/pkg/grouper" - - "github.com/google/go-cmp/cmp" - "github.com/google/osv-scanner/pkg/models" -) - -func TestGroup(t *testing.T) { - t.Parallel() - - // Should be grouped by IDs appearing in alias. - v1 := grouper.IDAliases{ - ID: "CVE-1", - Aliases: []string{ - "FOO-1", - }, - } - v2 := grouper.IDAliases{ - ID: "FOO-1", - Aliases: []string{}, - } - v3 := grouper.IDAliases{ - ID: "FOO-2", - Aliases: []string{ - "FOO-1", - }, - } - - // Should be grouped by aliases intersecting. - v4 := grouper.IDAliases{ - ID: "BAR-1", - Aliases: []string{ - "CVE-2", - "CVE-3", - }, - } - v5 := grouper.IDAliases{ - ID: "BAR-2", - Aliases: []string{ - "CVE-3", - "CVE-4", - }, - } - v6 := grouper.IDAliases{ - ID: "BAR-3", - Aliases: []string{ - "CVE-4", - }, - } - - // Unrelated. - v7 := grouper.IDAliases{ - ID: "UNRELATED-1", - Aliases: []string{ - "BAR-1337", - }, - } - v8 := grouper.IDAliases{ - ID: "UNRELATED-2", - Aliases: []string{ - "BAR-1338", - }, - } - - // Unrelated, empty aliases - v9 := grouper.IDAliases{ - ID: "UNRELATED-3", - } - v10 := grouper.IDAliases{ - ID: "UNRELATED-4", - } - for _, tc := range []struct { - vulns []grouper.IDAliases - want []models.GroupInfo - }{ - { - vulns: []grouper.IDAliases{ - v1, v2, v3, v4, v5, v6, v7, v8, - }, - want: []models.GroupInfo{ - { - IDs: []string{v1.ID, v2.ID, v3.ID}, - Aliases: []string{v1.ID, v2.ID, v3.ID}, - }, - { - IDs: []string{v4.ID, v5.ID, v6.ID}, - Aliases: []string{v4.ID, v5.ID, v6.ID, v4.Aliases[0], v4.Aliases[1], v5.Aliases[1]}, - }, - { - IDs: []string{v7.ID}, - Aliases: []string{v7.Aliases[0], v7.ID}, - }, - { - IDs: []string{v8.ID}, - Aliases: []string{v8.Aliases[0], v8.ID}, - }, - }, - }, - { - vulns: []grouper.IDAliases{ - v8, v2, v1, v5, v7, v4, v6, v3, v9, v10, - }, - want: []models.GroupInfo{ - { - IDs: []string{v8.ID}, - Aliases: []string{v8.Aliases[0], v8.ID}, - }, - { - IDs: []string{v1.ID, v2.ID, v3.ID}, // Deterministic order - Aliases: []string{v1.ID, v2.ID, v3.ID}, // Deterministic order - }, - { - IDs: []string{v4.ID, v5.ID, v6.ID}, - Aliases: []string{v4.ID, v5.ID, v6.ID, v4.Aliases[0], v4.Aliases[1], v5.Aliases[1]}, - }, - { - IDs: []string{v7.ID}, - Aliases: []string{v7.Aliases[0], v7.ID}, - }, - { - IDs: []string{v9.ID}, - Aliases: []string{v9.ID}, - }, - { - IDs: []string{v10.ID}, - Aliases: []string{v10.ID}, - }, - }, - }, - { - vulns: []grouper.IDAliases{ - v9, v10, - }, - want: []models.GroupInfo{ - { - IDs: []string{v9.ID}, - Aliases: []string{v9.ID}, - }, - { - IDs: []string{v10.ID}, - Aliases: []string{v10.ID}, - }, - }, - }, - } { - grouped := grouper.Group(tc.vulns) - if diff := cmp.Diff(tc.want, grouped); diff != "" { - t.Errorf("GroupedVulns() returned an unexpected result (-want +got):\n%s", diff) - } - } -} diff --git a/pkg/osvscanner/osvscanner.go b/pkg/osvscanner/osvscanner.go index e8ecb07b2a..f94a5e6844 100644 --- a/pkg/osvscanner/osvscanner.go +++ b/pkg/osvscanner/osvscanner.go @@ -2,6 +2,8 @@ package osvscanner import ( "bufio" + "cmp" + "context" "crypto/md5" //nolint:gosec "errors" "fmt" @@ -11,15 +13,21 @@ import ( "path" "path/filepath" "slices" - "sort" "strings" + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem/os/apk" + "github.com/google/osv-scalibr/extractor/filesystem/os/dpkg" + scalibrosv "github.com/google/osv-scalibr/extractor/filesystem/osv" + "github.com/google/osv-scanner/internal/config" "github.com/google/osv-scanner/internal/customgitignore" "github.com/google/osv-scanner/internal/depsdev" "github.com/google/osv-scanner/internal/image" "github.com/google/osv-scanner/internal/local" - "github.com/google/osv-scanner/internal/manifest" + "github.com/google/osv-scanner/internal/lockfilescalibr" + "github.com/google/osv-scanner/internal/lockfilescalibr/language/java/pomxmlnet" + "github.com/google/osv-scanner/internal/lockfilescalibr/language/osv/osvscannerjson" "github.com/google/osv-scanner/internal/output" "github.com/google/osv-scanner/internal/resolution/client" "github.com/google/osv-scanner/internal/resolution/datasource" @@ -37,16 +45,16 @@ import ( ) type ScannerActions struct { - LockfilePaths []string - SBOMPaths []string - DirectoryPaths []string - GitCommits []string - Recursive bool - SkipGit bool - NoIgnore bool - DockerContainerNames []string - ConfigOverridePath string - CallAnalysisStates map[string]bool + LockfilePaths []string + SBOMPaths []string + DirectoryPaths []string + GitCommits []string + Recursive bool + SkipGit bool + NoIgnore bool + DockerImageName string + ConfigOverridePath string + CallAnalysisStates map[string]bool ExperimentalScannerActions } @@ -171,17 +179,19 @@ func scanDir(r reporter.Reporter, dir string, skipGit bool, recursive bool, useG } if !info.IsDir() { - if extractor, _ := lockfile.FindExtractor(path, ""); extractor != nil { - pkgs, err := scanLockfile(r, path, "", transitiveAct) - if err != nil { + pkgs, err := scanLockfile(r, path, "", transitiveAct) + if err != nil { + // If no extractors found then just continue + if !errors.Is(err, lockfilescalibr.ErrNoExtractorsFound) { r.Errorf("Attempted to scan lockfile but failed: %s\n", path) } - scannedPackages = append(scannedPackages, pkgs...) } + scannedPackages = append(scannedPackages, pkgs...) + // No need to check for error // If scan fails, it means it isn't a valid SBOM file, // so just move onto the next file - pkgs, _ := scanSBOMFile(r, path, true) + pkgs, _ = scanSBOMFile(r, path, true) scannedPackages = append(scannedPackages, pkgs...) } @@ -356,27 +366,29 @@ func scanImage(r reporter.Reporter, path string) ([]scannedPackage, error) { // within to `query` func scanLockfile(r reporter.Reporter, path string, parseAs string, transitiveAct TransitiveScanningActions) ([]scannedPackage, error) { var err error - var parsedLockfile lockfile.Lockfile - - f, err := lockfile.OpenLocalDepFile(path) - - if err == nil { - // special case for the APK and DPKG parsers because they have a very generic name while - // living at a specific location, so they are not included in the map of parsers - // used by lockfile.Parse to avoid false-positives when scanning projects - switch parseAs { - case "apk-installed": - parsedLockfile, err = lockfile.FromApkInstalled(path) - case "dpkg-status": - parsedLockfile, err = lockfile.FromDpkgStatus(path) - case "osv-scanner": - parsedLockfile, err = lockfile.FromOSVScannerResults(path) - default: - if !transitiveAct.Disabled && (parseAs == "pom.xml" || filepath.Base(path) == "pom.xml") { - parsedLockfile, err = extractMavenDeps(f, transitiveAct) - } else { - parsedLockfile, err = lockfile.ExtractDeps(f, parseAs) + + var inventories []*extractor.Inventory + + // special case for the APK and DPKG parsers because they have a very generic name while + // living at a specific location, so they are not included in the map of parsers + // used by lockfile.Parse to avoid false-positives when scanning projects + switch parseAs { + case "apk-installed": + inventories, err = lockfilescalibr.ExtractWithExtractor(context.Background(), path, apk.New(apk.DefaultConfig())) + case "dpkg-status": + inventories, err = lockfilescalibr.ExtractWithExtractor(context.Background(), path, dpkg.New(dpkg.DefaultConfig())) + case "osv-scanner": + inventories, err = lockfilescalibr.ExtractWithExtractor(context.Background(), path, osvscannerjson.Extractor{}) + default: + if !transitiveAct.Disabled && (parseAs == "pom.xml" || filepath.Base(path) == "pom.xml") { + ext, extErr := createMavenExtractor(transitiveAct) + if extErr != nil { + return nil, extErr } + + inventories, err = lockfilescalibr.ExtractWithExtractor(context.Background(), path, ext) + } else { + inventories, err = lockfilescalibr.Extract(context.Background(), path, parseAs) } } @@ -390,33 +402,57 @@ func scanLockfile(r reporter.Reporter, path string, parseAs string, transitiveAc parsedAsComment = fmt.Sprintf("as a %s ", parseAs) } + slices.SortFunc(inventories, func(i, j *extractor.Inventory) int { + return cmp.Or( + strings.Compare(i.Name, j.Name), + strings.Compare(i.Version, j.Version), + ) + }) + + pkgCount := len(inventories) + r.Infof( "Scanned %s file %sand found %d %s\n", path, parsedAsComment, - len(parsedLockfile.Packages), - output.Form(len(parsedLockfile.Packages), "package", "packages"), + pkgCount, + output.Form(pkgCount, "package", "packages"), ) - packages := make([]scannedPackage, len(parsedLockfile.Packages)) - for i, pkgDetail := range parsedLockfile.Packages { - packages[i] = scannedPackage{ - Name: pkgDetail.Name, - Version: pkgDetail.Version, - Commit: pkgDetail.Commit, - Ecosystem: pkgDetail.Ecosystem, - DepGroups: pkgDetail.DepGroups, + packages := make([]scannedPackage, 0, pkgCount) + + for _, inv := range inventories { + scannedPackage := scannedPackage{ + Name: inv.Name, + Version: inv.Version, Source: models.SourceInfo{ Path: path, Type: "lockfile", }, } + if inv.SourceCode != nil { + scannedPackage.Commit = inv.SourceCode.Commit + } + eco := inv.Ecosystem() + // TODO(rexpan): Refactor these minor patches to individual items + // TODO: Ecosystem should be pared with Enum : Suffix + if eco == "Alpine" { + eco = "Alpine:v3.20" + } + + scannedPackage.Ecosystem = lockfile.Ecosystem(eco) + + if dg, ok := inv.Metadata.(scalibrosv.DepGroups); ok { + scannedPackage.DepGroups = dg.DepGroups() + } + + packages = append(packages, scannedPackage) } return packages, nil } -func extractMavenDeps(f lockfile.DepFile, actions TransitiveScanningActions) (lockfile.Lockfile, error) { +func createMavenExtractor(actions TransitiveScanningActions) (*pomxmlnet.Extractor, error) { var depClient client.DependencyClient var err error if actions.NativeDataSource { @@ -425,37 +461,20 @@ func extractMavenDeps(f lockfile.DepFile, actions TransitiveScanningActions) (lo depClient, err = client.NewDepsDevClient(depsdev.DepsdevAPI) } if err != nil { - return lockfile.Lockfile{}, err + return nil, err } mavenClient, err := datasource.NewMavenRegistryAPIClient(actions.MavenRegistry) if err != nil { - return lockfile.Lockfile{}, err + return nil, err } - extractor := manifest.MavenResolverExtractor{ + extractor := pomxmlnet.Extractor{ DependencyClient: depClient, MavenRegistryAPIClient: mavenClient, } - packages, err := extractor.Extract(f) - if err != nil { - err = fmt.Errorf("failed extracting %s: %w", f.Path(), err) - } - - // Sort packages for testing convenience. - sort.Slice(packages, func(i, j int) bool { - if packages[i].Name == packages[j].Name { - return packages[i].Version < packages[j].Version - } - - return packages[i].Name < packages[j].Name - }) - return lockfile.Lockfile{ - FilePath: f.Path(), - ParsedAs: "pom.xml", - Packages: packages, - }, err + return &extractor, nil } // scanSBOMFile will load, identify, and parse the SBOM path passed in, and add the dependencies specified @@ -644,72 +663,77 @@ func createCommitQueryPackage(commit string, source string) scannedPackage { } } -func scanDebianDocker(r reporter.Reporter, dockerImageName string) ([]scannedPackage, error) { - cmd := exec.Command("docker", "run", "--rm", "--entrypoint", "/usr/bin/dpkg-query", dockerImageName, "-f", "${Package}###${Version}\\n", "-W") - stdout, err := cmd.StdoutPipe() +func runCommandLogError(r reporter.Reporter, name string, args ...string) error { + cmd := exec.Command(name, args...) + // Get stderr for debugging when docker fails + stderr, err := cmd.StderrPipe() if err != nil { - r.Errorf("Failed to get stdout: %s\n", err) + r.Errorf("Failed to get stderr: %s\n", err) + return err + } + + err = cmd.Start() + if err != nil { + r.Errorf("Failed to run docker command (%q): %s\n", cmd.String(), err) + return err + } + // This has to be captured before cmd.Wait() is called, as cmd.Wait() closes the stderr pipe. + var stderrLines []string + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + stderrLines = append(stderrLines, scanner.Text()) + } + + err = cmd.Wait() + if err != nil { + r.Errorf("Docker command exited with code (%q): %d\nSTDERR:\n", cmd.String(), cmd.ProcessState.ExitCode()) + for _, line := range stderrLines { + r.Errorf("> %s\n", line) + } + + return errors.New("failed to run docker command") + } + + return nil +} + +func scanDockerImage(r reporter.Reporter, dockerImageName string) ([]scannedPackage, error) { + tempImageFile, err := os.CreateTemp("", "docker-image-*.tar") + if err != nil { + r.Errorf("Failed to create temporary file: %s\n", err) return nil, err } - stderr, err := cmd.StderrPipe() + err = tempImageFile.Close() if err != nil { - r.Errorf("Failed to get stderr: %s\n", err) return nil, err } + defer os.Remove(tempImageFile.Name()) - err = cmd.Start() + r.Infof("Pulling docker image (%q)...\n", dockerImageName) + err = runCommandLogError(r, "docker", "pull", "-q", dockerImageName) if err != nil { - r.Errorf("Failed to start docker image: %s\n", err) return nil, err } - defer func() { - var stderrlines []string - scanner := bufio.NewScanner(stderr) - for scanner.Scan() { - stderrlines = append(stderrlines, scanner.Text()) - } + r.Infof("Saving docker image (%q) to temporary file...\n", dockerImageName) + err = runCommandLogError(r, "docker", "save", "-o", tempImageFile.Name(), dockerImageName) + if err != nil { + return nil, err + } - err := cmd.Wait() - if err != nil { - r.Errorf("Docker command exited with code %d\n", cmd.ProcessState.ExitCode()) - for _, line := range stderrlines { - r.Errorf("> %s\n", line) - } - } - }() + r.Infof("Scanning image...\n") + packages, err := scanImage(r, tempImageFile.Name()) + if err != nil { + return nil, err + } - scanner := bufio.NewScanner(stdout) - var packages []scannedPackage - for scanner.Scan() { - text := scanner.Text() - text = strings.TrimSpace(text) - if len(text) == 0 { - continue - } - splitText := strings.Split(text, "###") - if len(splitText) != 2 { - r.Errorf("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 - packages = append(packages, scannedPackage{ - Name: splitText[0], - Version: splitText[1], - Ecosystem: "Debian", - Source: models.SourceInfo{ - Path: dockerImageName, - Type: "docker", - }, - }) + // Modify the image path to be the image name, rather than the temporary file name + for i := range packages { + _, internalPath, _ := strings.Cut(packages[i].Source.Path, ":") + packages[i].Source.Path = dockerImageName + ":" + internalPath } - r.Infof( - "Scanned docker image with %d %s\n", - len(packages), - output.Form(len(packages), "package", "packages"), - ) return packages, nil } @@ -859,9 +883,11 @@ func DoScan(actions ScannerActions, r reporter.Reporter) (models.VulnerabilityRe scannedPackages = append(scannedPackages, pkgs...) } - // TODO: Deprecated - for _, container := range actions.DockerContainerNames { - pkgs, _ := scanDebianDocker(r, container) + if actions.DockerImageName != "" { + pkgs, err := scanDockerImage(r, actions.DockerImageName) + if err != nil { + return models.VulnerabilityResults{}, err + } scannedPackages = append(scannedPackages, pkgs...) } @@ -1012,7 +1038,12 @@ func filterIgnoredPackages(r reporter.Reporter, packages []scannedPackage, confi } if ignore, ignoreLine := configToUse.ShouldIgnorePackage(pkg); ignore { - pkgString := fmt.Sprintf("%s/%s/%s", p.Ecosystem, p.Name, p.Version) + var pkgString string + if p.PURL != "" { + pkgString = p.PURL + } else { + pkgString = fmt.Sprintf("%s/%s/%s", p.Ecosystem, p.Name, p.Version) + } reason := ignoreLine.Reason if reason == "" { diff --git a/pkg/spdx/gen.go b/pkg/spdx/gen.go deleted file mode 100644 index 8c7daefd31..0000000000 --- a/pkg/spdx/gen.go +++ /dev/null @@ -1,62 +0,0 @@ -//go:build generate -// +build generate - -//go:generate go run gen.go - -package main - -import ( - "encoding/json" - "fmt" - "go/format" - "io/ioutil" - "net/http" - "strings" -) - -type License struct { - SPDXID string `json:"licenseId"` -} - -func main() { - resp, err := http.Get("https://raw.githubusercontent.com/spdx/license-list-data/main/json/licenses.json") - if err != nil { - panic(err) - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - panic(err) - } - - var licenseList struct { - Licenses []License `json:"licenses"` - } - err = json.Unmarshal(body, &licenseList) - if err != nil { - panic(err) - } - - output := strings.TrimLeft(` -// Code generated by gen.go. DO NOT EDIT. -// -// Deprecated: this is now private and should not be used outside the scanner -package spdx - -// Deprecated: this is now private and should not be used outside the scanner -var IDs = map[string]bool{ -`, "\n") - for _, license := range licenseList.Licenses { - output += fmt.Sprintf("%q: true,\n", strings.ToLower(license.SPDXID)) - } - output += "}" - formatted, err := format.Source([]byte(output)) - if err != nil { - panic(err) - } - err = ioutil.WriteFile("licenses.go", formatted, 0644) - if err != nil { - panic(err) - } -} diff --git a/pkg/spdx/licenses.go b/pkg/spdx/licenses.go deleted file mode 100644 index c389b69086..0000000000 --- a/pkg/spdx/licenses.go +++ /dev/null @@ -1,679 +0,0 @@ -// Code generated by gen.go. DO NOT EDIT. -// -// Deprecated: this is now private and should not be used outside the scanner -package spdx - -// Deprecated: this is now private and should not be used outside the scanner -var IDs = map[string]bool{ - "0bsd": true, - "3d-slicer-1.0": true, - "aal": true, - "abstyles": true, - "adacore-doc": true, - "adobe-2006": true, - "adobe-display-postscript": true, - "adobe-glyph": true, - "adobe-utopia": true, - "adsl": true, - "afl-1.1": true, - "afl-1.2": true, - "afl-2.0": true, - "afl-2.1": true, - "afl-3.0": true, - "afmparse": true, - "agpl-1.0": true, - "agpl-1.0-only": true, - "agpl-1.0-or-later": true, - "agpl-3.0": true, - "agpl-3.0-only": true, - "agpl-3.0-or-later": true, - "aladdin": true, - "amd-newlib": true, - "amdplpa": true, - "aml": true, - "aml-glslang": true, - "ampas": true, - "antlr-pd": true, - "antlr-pd-fallback": true, - "any-osi": true, - "apache-1.0": true, - "apache-1.1": true, - "apache-2.0": true, - "apafml": true, - "apl-1.0": true, - "app-s2p": true, - "apsl-1.0": true, - "apsl-1.1": true, - "apsl-1.2": true, - "apsl-2.0": true, - "arphic-1999": true, - "artistic-1.0": true, - "artistic-1.0-cl8": true, - "artistic-1.0-perl": true, - "artistic-2.0": true, - "aswf-digital-assets-1.0": true, - "aswf-digital-assets-1.1": true, - "baekmuk": true, - "bahyph": true, - "barr": true, - "bcrypt-solar-designer": true, - "beerware": true, - "bitstream-charter": true, - "bitstream-vera": true, - "bittorrent-1.0": true, - "bittorrent-1.1": true, - "blessing": true, - "blueoak-1.0.0": true, - "boehm-gc": true, - "boehm-gc-without-fee": true, - "borceux": true, - "brian-gladman-2-clause": true, - "brian-gladman-3-clause": true, - "bsd-1-clause": true, - "bsd-2-clause": true, - "bsd-2-clause-darwin": true, - "bsd-2-clause-first-lines": true, - "bsd-2-clause-freebsd": true, - "bsd-2-clause-netbsd": true, - "bsd-2-clause-patent": true, - "bsd-2-clause-views": true, - "bsd-3-clause": true, - "bsd-3-clause-acpica": true, - "bsd-3-clause-attribution": true, - "bsd-3-clause-clear": true, - "bsd-3-clause-flex": true, - "bsd-3-clause-hp": true, - "bsd-3-clause-lbnl": true, - "bsd-3-clause-modification": true, - "bsd-3-clause-no-military-license": true, - "bsd-3-clause-no-nuclear-license": true, - "bsd-3-clause-no-nuclear-license-2014": true, - "bsd-3-clause-no-nuclear-warranty": true, - "bsd-3-clause-open-mpi": true, - "bsd-3-clause-sun": true, - "bsd-4-clause": true, - "bsd-4-clause-shortened": true, - "bsd-4-clause-uc": true, - "bsd-4.3reno": true, - "bsd-4.3tahoe": true, - "bsd-advertising-acknowledgement": true, - "bsd-attribution-hpnd-disclaimer": true, - "bsd-inferno-nettverk": true, - "bsd-protection": true, - "bsd-source-beginning-file": true, - "bsd-source-code": true, - "bsd-systemics": true, - "bsd-systemics-w3works": true, - "bsl-1.0": true, - "busl-1.1": true, - "bzip2-1.0.5": true, - "bzip2-1.0.6": true, - "c-uda-1.0": true, - "cal-1.0": true, - "cal-1.0-combined-work-exception": true, - "caldera": true, - "caldera-no-preamble": true, - "catharon": true, - "catosl-1.1": true, - "cc-by-1.0": true, - "cc-by-2.0": true, - "cc-by-2.5": true, - "cc-by-2.5-au": true, - "cc-by-3.0": true, - "cc-by-3.0-at": true, - "cc-by-3.0-au": true, - "cc-by-3.0-de": true, - "cc-by-3.0-igo": true, - "cc-by-3.0-nl": true, - "cc-by-3.0-us": true, - "cc-by-4.0": true, - "cc-by-nc-1.0": true, - "cc-by-nc-2.0": true, - "cc-by-nc-2.5": true, - "cc-by-nc-3.0": true, - "cc-by-nc-3.0-de": true, - "cc-by-nc-4.0": true, - "cc-by-nc-nd-1.0": true, - "cc-by-nc-nd-2.0": true, - "cc-by-nc-nd-2.5": true, - "cc-by-nc-nd-3.0": true, - "cc-by-nc-nd-3.0-de": true, - "cc-by-nc-nd-3.0-igo": true, - "cc-by-nc-nd-4.0": true, - "cc-by-nc-sa-1.0": true, - "cc-by-nc-sa-2.0": true, - "cc-by-nc-sa-2.0-de": true, - "cc-by-nc-sa-2.0-fr": true, - "cc-by-nc-sa-2.0-uk": true, - "cc-by-nc-sa-2.5": true, - "cc-by-nc-sa-3.0": true, - "cc-by-nc-sa-3.0-de": true, - "cc-by-nc-sa-3.0-igo": true, - "cc-by-nc-sa-4.0": true, - "cc-by-nd-1.0": true, - "cc-by-nd-2.0": true, - "cc-by-nd-2.5": true, - "cc-by-nd-3.0": true, - "cc-by-nd-3.0-de": true, - "cc-by-nd-4.0": true, - "cc-by-sa-1.0": true, - "cc-by-sa-2.0": true, - "cc-by-sa-2.0-uk": true, - "cc-by-sa-2.1-jp": true, - "cc-by-sa-2.5": true, - "cc-by-sa-3.0": true, - "cc-by-sa-3.0-at": true, - "cc-by-sa-3.0-de": true, - "cc-by-sa-3.0-igo": true, - "cc-by-sa-4.0": true, - "cc-pddc": true, - "cc0-1.0": true, - "cddl-1.0": true, - "cddl-1.1": true, - "cdl-1.0": true, - "cdla-permissive-1.0": true, - "cdla-permissive-2.0": true, - "cdla-sharing-1.0": true, - "cecill-1.0": true, - "cecill-1.1": true, - "cecill-2.0": true, - "cecill-2.1": true, - "cecill-b": true, - "cecill-c": true, - "cern-ohl-1.1": true, - "cern-ohl-1.2": true, - "cern-ohl-p-2.0": true, - "cern-ohl-s-2.0": true, - "cern-ohl-w-2.0": true, - "cfitsio": true, - "check-cvs": true, - "checkmk": true, - "clartistic": true, - "clips": true, - "cmu-mach": true, - "cmu-mach-nodoc": true, - "cnri-jython": true, - "cnri-python": true, - "cnri-python-gpl-compatible": true, - "coil-1.0": true, - "community-spec-1.0": true, - "condor-1.1": true, - "copyleft-next-0.3.0": true, - "copyleft-next-0.3.1": true, - "cornell-lossless-jpeg": true, - "cpal-1.0": true, - "cpl-1.0": true, - "cpol-1.02": true, - "cronyx": true, - "crossword": true, - "crystalstacker": true, - "cua-opl-1.0": true, - "cube": true, - "curl": true, - "cve-tou": true, - "d-fsl-1.0": true, - "dec-3-clause": true, - "diffmark": true, - "dl-de-by-2.0": true, - "dl-de-zero-2.0": true, - "doc": true, - "docbook-schema": true, - "docbook-stylesheet": true, - "docbook-xml": true, - "dotseqn": true, - "drl-1.0": true, - "drl-1.1": true, - "dsdp": true, - "dtoa": true, - "dvipdfm": true, - "ecl-1.0": true, - "ecl-2.0": true, - "ecos-2.0": true, - "efl-1.0": true, - "efl-2.0": true, - "egenix": true, - "elastic-2.0": true, - "entessa": true, - "epics": true, - "epl-1.0": true, - "epl-2.0": true, - "erlpl-1.1": true, - "etalab-2.0": true, - "eudatagrid": true, - "eupl-1.0": true, - "eupl-1.1": true, - "eupl-1.2": true, - "eurosym": true, - "fair": true, - "fbm": true, - "fdk-aac": true, - "ferguson-twofish": true, - "frameworx-1.0": true, - "freebsd-doc": true, - "freeimage": true, - "fsfap": true, - "fsfap-no-warranty-disclaimer": true, - "fsful": true, - "fsfullr": true, - "fsfullrwd": true, - "ftl": true, - "furuseth": true, - "fwlw": true, - "gcr-docs": true, - "gd": true, - "gfdl-1.1": true, - "gfdl-1.1-invariants-only": true, - "gfdl-1.1-invariants-or-later": true, - "gfdl-1.1-no-invariants-only": true, - "gfdl-1.1-no-invariants-or-later": true, - "gfdl-1.1-only": true, - "gfdl-1.1-or-later": true, - "gfdl-1.2": true, - "gfdl-1.2-invariants-only": true, - "gfdl-1.2-invariants-or-later": true, - "gfdl-1.2-no-invariants-only": true, - "gfdl-1.2-no-invariants-or-later": true, - "gfdl-1.2-only": true, - "gfdl-1.2-or-later": true, - "gfdl-1.3": true, - "gfdl-1.3-invariants-only": true, - "gfdl-1.3-invariants-or-later": true, - "gfdl-1.3-no-invariants-only": true, - "gfdl-1.3-no-invariants-or-later": true, - "gfdl-1.3-only": true, - "gfdl-1.3-or-later": true, - "giftware": true, - "gl2ps": true, - "glide": true, - "glulxe": true, - "glwtpl": true, - "gnuplot": true, - "gpl-1.0": true, - "gpl-1.0+": true, - "gpl-1.0-only": true, - "gpl-1.0-or-later": true, - "gpl-2.0": true, - "gpl-2.0+": true, - "gpl-2.0-only": true, - "gpl-2.0-or-later": true, - "gpl-2.0-with-autoconf-exception": true, - "gpl-2.0-with-bison-exception": true, - "gpl-2.0-with-classpath-exception": true, - "gpl-2.0-with-font-exception": true, - "gpl-2.0-with-gcc-exception": true, - "gpl-3.0": true, - "gpl-3.0+": true, - "gpl-3.0-only": true, - "gpl-3.0-or-later": true, - "gpl-3.0-with-autoconf-exception": true, - "gpl-3.0-with-gcc-exception": true, - "graphics-gems": true, - "gsoap-1.3b": true, - "gtkbook": true, - "gutmann": true, - "haskellreport": true, - "hdparm": true, - "hidapi": true, - "hippocratic-2.1": true, - "hp-1986": true, - "hp-1989": true, - "hpnd": true, - "hpnd-dec": true, - "hpnd-doc": true, - "hpnd-doc-sell": true, - "hpnd-export-us": true, - "hpnd-export-us-acknowledgement": true, - "hpnd-export-us-modify": true, - "hpnd-export2-us": true, - "hpnd-fenneberg-livingston": true, - "hpnd-inria-imag": true, - "hpnd-intel": true, - "hpnd-kevlin-henney": true, - "hpnd-markus-kuhn": true, - "hpnd-merchantability-variant": true, - "hpnd-mit-disclaimer": true, - "hpnd-netrek": true, - "hpnd-pbmplus": true, - "hpnd-sell-mit-disclaimer-xserver": true, - "hpnd-sell-regexpr": true, - "hpnd-sell-variant": true, - "hpnd-sell-variant-mit-disclaimer": true, - "hpnd-sell-variant-mit-disclaimer-rev": true, - "hpnd-uc": true, - "hpnd-uc-export-us": true, - "htmltidy": true, - "ibm-pibs": true, - "icu": true, - "iec-code-components-eula": true, - "ijg": true, - "ijg-short": true, - "imagemagick": true, - "imatix": true, - "imlib2": true, - "info-zip": true, - "inner-net-2.0": true, - "intel": true, - "intel-acpi": true, - "interbase-1.0": true, - "ipa": true, - "ipl-1.0": true, - "isc": true, - "isc-veillard": true, - "jam": true, - "jasper-2.0": true, - "jpl-image": true, - "jpnic": true, - "json": true, - "kastrup": true, - "kazlib": true, - "knuth-ctan": true, - "lal-1.2": true, - "lal-1.3": true, - "latex2e": true, - "latex2e-translated-notice": true, - "leptonica": true, - "lgpl-2.0": true, - "lgpl-2.0+": true, - "lgpl-2.0-only": true, - "lgpl-2.0-or-later": true, - "lgpl-2.1": true, - "lgpl-2.1+": true, - "lgpl-2.1-only": true, - "lgpl-2.1-or-later": true, - "lgpl-3.0": true, - "lgpl-3.0+": true, - "lgpl-3.0-only": true, - "lgpl-3.0-or-later": true, - "lgpllr": true, - "libpng": true, - "libpng-2.0": true, - "libselinux-1.0": true, - "libtiff": true, - "libutil-david-nugent": true, - "liliq-p-1.1": true, - "liliq-r-1.1": true, - "liliq-rplus-1.1": true, - "linux-man-pages-1-para": true, - "linux-man-pages-copyleft": true, - "linux-man-pages-copyleft-2-para": true, - "linux-man-pages-copyleft-var": true, - "linux-openib": true, - "loop": true, - "lpd-document": true, - "lpl-1.0": true, - "lpl-1.02": true, - "lppl-1.0": true, - "lppl-1.1": true, - "lppl-1.2": true, - "lppl-1.3a": true, - "lppl-1.3c": true, - "lsof": true, - "lucida-bitmap-fonts": true, - "lzma-sdk-9.11-to-9.20": true, - "lzma-sdk-9.22": true, - "mackerras-3-clause": true, - "mackerras-3-clause-acknowledgment": true, - "magaz": true, - "mailprio": true, - "makeindex": true, - "martin-birgmeier": true, - "mcphee-slideshow": true, - "metamail": true, - "minpack": true, - "miros": true, - "mit": true, - "mit-0": true, - "mit-advertising": true, - "mit-click": true, - "mit-cmu": true, - "mit-enna": true, - "mit-feh": true, - "mit-festival": true, - "mit-khronos-old": true, - "mit-modern-variant": true, - "mit-open-group": true, - "mit-testregex": true, - "mit-wu": true, - "mitnfa": true, - "mmixware": true, - "motosoto": true, - "mpeg-ssg": true, - "mpi-permissive": true, - "mpich2": true, - "mpl-1.0": true, - "mpl-1.1": true, - "mpl-2.0": true, - "mpl-2.0-no-copyleft-exception": true, - "mplus": true, - "ms-lpl": true, - "ms-pl": true, - "ms-rl": true, - "mtll": true, - "mulanpsl-1.0": true, - "mulanpsl-2.0": true, - "multics": true, - "mup": true, - "naist-2003": true, - "nasa-1.3": true, - "naumen": true, - "nbpl-1.0": true, - "ncbi-pd": true, - "ncgl-uk-2.0": true, - "ncl": true, - "ncsa": true, - "net-snmp": true, - "netcdf": true, - "newsletr": true, - "ngpl": true, - "nicta-1.0": true, - "nist-pd": true, - "nist-pd-fallback": true, - "nist-software": true, - "nlod-1.0": true, - "nlod-2.0": true, - "nlpl": true, - "nokia": true, - "nosl": true, - "noweb": true, - "npl-1.0": true, - "npl-1.1": true, - "nposl-3.0": true, - "nrl": true, - "ntp": true, - "ntp-0": true, - "nunit": true, - "o-uda-1.0": true, - "oar": true, - "occt-pl": true, - "oclc-2.0": true, - "odbl-1.0": true, - "odc-by-1.0": true, - "offis": true, - "ofl-1.0": true, - "ofl-1.0-no-rfn": true, - "ofl-1.0-rfn": true, - "ofl-1.1": true, - "ofl-1.1-no-rfn": true, - "ofl-1.1-rfn": true, - "ogc-1.0": true, - "ogdl-taiwan-1.0": true, - "ogl-canada-2.0": true, - "ogl-uk-1.0": true, - "ogl-uk-2.0": true, - "ogl-uk-3.0": true, - "ogtsl": true, - "oldap-1.1": true, - "oldap-1.2": true, - "oldap-1.3": true, - "oldap-1.4": true, - "oldap-2.0": true, - "oldap-2.0.1": true, - "oldap-2.1": true, - "oldap-2.2": true, - "oldap-2.2.1": true, - "oldap-2.2.2": true, - "oldap-2.3": true, - "oldap-2.4": true, - "oldap-2.5": true, - "oldap-2.6": true, - "oldap-2.7": true, - "oldap-2.8": true, - "olfl-1.3": true, - "oml": true, - "openpbs-2.3": true, - "openssl": true, - "openssl-standalone": true, - "openvision": true, - "opl-1.0": true, - "opl-uk-3.0": true, - "opubl-1.0": true, - "oset-pl-2.1": true, - "osl-1.0": true, - "osl-1.1": true, - "osl-2.0": true, - "osl-2.1": true, - "osl-3.0": true, - "padl": true, - "parity-6.0.0": true, - "parity-7.0.0": true, - "pddl-1.0": true, - "php-3.0": true, - "php-3.01": true, - "pixar": true, - "pkgconf": true, - "plexus": true, - "pnmstitch": true, - "polyform-noncommercial-1.0.0": true, - "polyform-small-business-1.0.0": true, - "postgresql": true, - "ppl": true, - "psf-2.0": true, - "psfrag": true, - "psutils": true, - "python-2.0": true, - "python-2.0.1": true, - "python-ldap": true, - "qhull": true, - "qpl-1.0": true, - "qpl-1.0-inria-2004": true, - "radvd": true, - "rdisc": true, - "rhecos-1.1": true, - "rpl-1.1": true, - "rpl-1.5": true, - "rpsl-1.0": true, - "rsa-md": true, - "rscpl": true, - "ruby": true, - "ruby-pty": true, - "sax-pd": true, - "sax-pd-2.0": true, - "saxpath": true, - "scea": true, - "schemereport": true, - "sendmail": true, - "sendmail-8.23": true, - "sendmail-open-source-1.1": true, - "sgi-b-1.0": true, - "sgi-b-1.1": true, - "sgi-b-2.0": true, - "sgi-opengl": true, - "sgp4": true, - "shl-0.5": true, - "shl-0.51": true, - "simpl-2.0": true, - "sissl": true, - "sissl-1.2": true, - "sl": true, - "sleepycat": true, - "smlnj": true, - "smppl": true, - "snia": true, - "snprintf": true, - "softsurfer": true, - "soundex": true, - "spencer-86": true, - "spencer-94": true, - "spencer-99": true, - "spl-1.0": true, - "ssh-keyscan": true, - "ssh-openssh": true, - "ssh-short": true, - "ssleay-standalone": true, - "sspl-1.0": true, - "standardml-nj": true, - "sugarcrm-1.1.3": true, - "sun-ppp": true, - "sun-ppp-2000": true, - "sunpro": true, - "swl": true, - "swrule": true, - "symlinks": true, - "tapr-ohl-1.0": true, - "tcl": true, - "tcp-wrappers": true, - "termreadkey": true, - "tgppl-1.0": true, - "threeparttable": true, - "tmate": true, - "torque-1.1": true, - "tosl": true, - "tpdl": true, - "tpl-1.0": true, - "trustedqsl": true, - "ttwl": true, - "ttyp0": true, - "tu-berlin-1.0": true, - "tu-berlin-2.0": true, - "ubuntu-font-1.0": true, - "ucar": true, - "ucl-1.0": true, - "ulem": true, - "umich-merit": true, - "unicode-3.0": true, - "unicode-dfs-2015": true, - "unicode-dfs-2016": true, - "unicode-tou": true, - "unixcrypt": true, - "unlicense": true, - "upl-1.0": true, - "urt-rle": true, - "vim": true, - "vostrom": true, - "vsl-1.0": true, - "w3c": true, - "w3c-19980720": true, - "w3c-20150513": true, - "w3m": true, - "watcom-1.0": true, - "widget-workshop": true, - "wsuipa": true, - "wtfpl": true, - "wxwindows": true, - "x11": true, - "x11-distribute-modifications-variant": true, - "x11-swapped": true, - "xdebug-1.03": true, - "xerox": true, - "xfig": true, - "xfree86-1.1": true, - "xinetd": true, - "xkeyboard-config-zinoviev": true, - "xlock": true, - "xnet": true, - "xpp": true, - "xskat": true, - "xzoom": true, - "ypl-1.0": true, - "ypl-1.1": true, - "zed": true, - "zeeff": true, - "zend-2.0": true, - "zimbra-1.3": true, - "zimbra-1.4": true, - "zlib": true, - "zlib-acknowledgement": true, - "zpl-1.1": true, - "zpl-2.0": true, - "zpl-2.1": true, -} diff --git a/pkg/spdx/verify.go b/pkg/spdx/verify.go deleted file mode 100644 index df36e621fc..0000000000 --- a/pkg/spdx/verify.go +++ /dev/null @@ -1,19 +0,0 @@ -// Deprecated: this is now private and should not be used outside the scanner -package spdx - -import "strings" - -// Unrecognized filters licenses for non-spdx identifiers. The "unknown" string is -// also treated as a valid identifier. -// -// Deprecated: this is now private and should not be used outside the scanner -func Unrecognized(licenses []string) (unrecognized []string) { - for _, license := range licenses { - l := strings.ToLower(license) - if !IDs[l] && l != "unknown" { - unrecognized = append(unrecognized, license) - } - } - - return unrecognized -} diff --git a/pkg/spdx/verify_test.go b/pkg/spdx/verify_test.go deleted file mode 100644 index f0e0cecd4b..0000000000 --- a/pkg/spdx/verify_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package spdx - -import ( - "reflect" - "testing" -) - -func Test_unrecognized(t *testing.T) { - t.Parallel() - tests := []struct { - name string - licenses []string - want []string - }{ - { - name: "all recognized licenses", - licenses: []string{"agpl-1.0", "MIT", "apache-1.0", "UNKNOWN"}, - want: nil, - }, { - name: "all unrecognized licenses", - licenses: []string{"agpl1.0", "unrecognized license", "apache1.0"}, - want: []string{"agpl1.0", "unrecognized license", "apache1.0"}, - }, { - name: "some recognized, some unrecognized licenses", - licenses: []string{"agpl-1.0", "unrecognized license", "apache-1.0"}, - want: []string{"unrecognized license"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - if got := Unrecognized(tt.licenses); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Unrecognized() = %v,\nwant %v", got, tt.want) - } - }) - } -}