diff --git a/.binny.yaml b/.binny.yaml index ec3fbb050a8..d73cbb63c33 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -2,7 +2,7 @@ tools: # we want to use a pinned version of binny to manage the toolchain (so binny manages itself!) - name: binny version: - want: v0.8.0 + want: v0.9.0 method: github-release with: repo: anchore/binny @@ -26,7 +26,7 @@ tools: # used for linting - name: golangci-lint version: - want: v1.64.6 + want: v2.1.6 method: github-release with: repo: golangci/golangci-lint @@ -42,7 +42,7 @@ tools: # used for signing the checksums file at release - name: cosign version: - want: v2.4.3 + want: v2.5.0 method: github-release with: repo: sigstore/cosign @@ -58,7 +58,7 @@ tools: # used to release all artifacts - name: goreleaser version: - want: v2.7.0 + want: v2.9.0 method: github-release with: repo: goreleaser/goreleaser @@ -90,7 +90,7 @@ tools: # used for running all local and CI tasks - name: task version: - want: v3.41.0 + want: v3.43.3 method: github-release with: repo: go-task/task @@ -98,7 +98,7 @@ tools: # used for triggering a release - name: gh version: - want: v2.68.0 + want: v2.72.0 method: github-release with: repo: cli/cli diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index 150284da53d..3a6a906469e 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -4,7 +4,7 @@ inputs: go-version: description: "Go version to install" required: true - default: "1.24.x" + default: ">= 1.24" python-version: description: "Python version to install" required: true @@ -32,18 +32,18 @@ runs: using: "composite" steps: # note: go mod and build is automatically cached on default with v4+ - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 if: inputs.go-version != '' with: go-version: ${{ inputs.go-version }} - - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ inputs.python-version }} - name: Restore tool cache id: tool-cache - uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 if: inputs.tools == 'true' with: path: ${{ github.workspace }}/.tool diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 67d1fc17e08..89657d393f1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -46,7 +46,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Utilize Go Module Cache - uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ~/go/pkg/mod @@ -56,14 +56,14 @@ jobs: ${{ runner.os }}-go- - name: Set correct version of Golang to use during CodeQL run - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: '1.21' check-latest: true # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -85,4 +85,4 @@ jobs: run: make grype - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c7364a00319..1c17ee739cf 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -60,6 +60,17 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} # This check name is defined as the github action job name (in .github/workflows/testing.yaml) checkName: "Integration tests" + timeoutSeconds: 1200 # 20 minutes, it sometimes takes that long + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Check integration test results + uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 + id: quality_tests + with: + token: ${{ secrets.GITHUB_TOKEN }} + # This check name is defined as the github action job name (in .github/workflows/testing.yaml) + checkName: "Quality tests" + timeoutSeconds: 1200 # 20 minutes, it sometimes takes that long ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check acceptance test results (linux) @@ -90,11 +101,12 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Quality gate - if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' + if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.quality_tests.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' run: | echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}" echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}" echo "Integration Test Status: ${{ steps.integration.outputs.conclusion }}" + echo "Quality Test Status: ${{ steps.quality_tests.outputs.conclusion }}" echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}" echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}" echo "CLI Test (Linux) Status: ${{ steps.cli-linux.outputs.conclusion }}" @@ -123,13 +135,13 @@ jobs: build-cache-key-prefix: "snapshot" - name: Login to Docker Hub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 #v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0 with: username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }} password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }} - name: Login to GitHub Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 #v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -158,12 +170,12 @@ jobs: # for updating brew formula in anchore/homebrew-syft GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }} - - uses: anchore/sbom-action@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0 + - uses: anchore/sbom-action@e11c554f704a0b820cbf8c51673f6945e0731532 # v0.20.0 continue-on-error: true with: artifact-name: sbom.spdx.json - - uses: 8398a7/action-slack@28ba43ae48961b90635b50953d216767a6bea486 # v3.16.2 + - uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e # v3.18.0 continue-on-error: true with: status: ${{ job.status }} diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 10c014b21a3..8d90e6f855e 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -38,6 +38,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v1.0.26 + uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v1.0.26 with: sarif_file: results.sarif diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index b3b540e69d8..75c3340bff5 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -60,7 +60,7 @@ jobs: - name: Upload the provider state archive if: ${{ failure() }} - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: qg-capture-state path: qg-capture-state.tar.gz @@ -103,7 +103,7 @@ jobs: uses: ./.github/actions/bootstrap - name: Restore integration test cache - uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3 with: path: ${{ github.workspace }}/test/integration/test-fixtures/cache key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }} @@ -134,11 +134,51 @@ jobs: # why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach). # see https://github.com/actions/upload-artifact/issues/199 for more info - name: Upload snapshot artifacts - uses: actions/cache/save@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2 + uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3 with: path: snapshot key: snapshot-build-${{ github.run_id }} + Upload-Snapshot-Artifacts: + name: "Upload snapshot artifacts" + needs: [Build-Snapshot-Artifacts] + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 + + - name: Download snapshot build + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3 + with: + path: snapshot + key: snapshot-build-${{ github.run_id }} + + - run: npm install @actions/artifact@2.2.2 + + - uses: actions/github-script@v7 + with: + script: | + const { readdirSync } = require('fs') + const { DefaultArtifactClient } = require('@actions/artifact') + const artifact = new DefaultArtifactClient() + const ls = d => readdirSync(d, { withFileTypes: true }) + const baseDir = "./snapshot" + const dirs = ls(baseDir).filter(f => f.isDirectory()).map(f => f.name) + const uploads = [] + for (const dir of dirs) { + // uploadArtifact returns Promise<{id, size}> + uploads.push(artifact.uploadArtifact( + // name of the archive: + `${dir}`, + // array of all files to include: + ls(`${baseDir}/${dir}`).map(f => `${baseDir}/${dir}/${f.name}`), + // base directory to trim from entries: + `${baseDir}/${dir}`, + { retentionDays: 30 } + )) + } + // wait for all uploads to finish + Promise.all(uploads) + Acceptance-Linux: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Acceptance tests (Linux)" @@ -148,14 +188,14 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 - name: Download snapshot build - uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2 + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3 with: path: snapshot key: snapshot-build-${{ github.run_id }} - name: Restore install.sh test image cache id: install-test-image-cache - uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3 with: path: ${{ github.workspace }}/test/install/cache key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }} @@ -178,19 +218,19 @@ jobs: runs-on: macos-latest steps: - name: Install Cosign - uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a #v3.8.1 + uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb #v3.8.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 - name: Download snapshot build - uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2 + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3 with: path: snapshot key: snapshot-build-${{ github.run_id }} - name: Restore docker image cache for compare testing id: mac-compare-testing-cache - uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3 with: path: image.tar key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }} @@ -211,16 +251,35 @@ jobs: uses: ./.github/actions/bootstrap - name: Restore CLI test-fixture cache - uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3 with: path: ${{ github.workspace }}/test/cli/test-fixtures/cache key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }} - name: Download snapshot build - uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2 + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3 with: path: snapshot key: snapshot-build-${{ github.run_id }} - name: Run CLI Tests (Linux) run: make cli + + Cleanup-Cache: + name: "Cleanup snapshot cache" + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: + - Acceptance-Linux + - Acceptance-Mac + - Cli-Linux + - Upload-Snapshot-Artifacts + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 + + - name: Delete snapshot cache + run: gh cache delete "snapshot-build-${{ github.run_id }}" + env: + GH_TOKEN: ${{ github.token }} diff --git a/.golangci.yaml b/.golangci.yaml index 6521a59d3ac..68be2075aa3 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,57 +1,46 @@ -issues: - max-same-issues: 25 - uniq-by-line: false - - # TODO: enable this when we have coverage on docstring comments -# # The list of ids of default excludes to include or disable. -# include: -# - EXC0002 # disable excluding of issues about comments from golint - +version: "2" linters: - # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint - disable-all: true + # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint + default: none enable: - asciicheck - bodyclose + - copyloopvar - dogsled - dupl - errcheck - - copyloopvar - funlen - gocognit - goconst - gocritic - gocyclo - - gofmt - - goimports - goprintffuncname - gosec - - gosimple - govet - ineffassign - misspell - nakedret - revive - staticcheck - - stylecheck - - typecheck - unconvert - unparam - unused - whitespace - -linters-settings: - funlen: - # Checks the number of lines in a function. - # If lower than 0, disable the check. - # Default: 60 - lines: 70 - # Checks the number of statements in a function. - # If lower than 0, disable the check. - # Default: 40 - statements: 50 -run: - timeout: 10m + settings: + funlen: + lines: 70 + statements: 50 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ # do not enable... # - deadcode # The owner seems to have abandoned the linter. Replaced by "unused". @@ -79,3 +68,23 @@ run: # - testpackage # - varcheck # The owner seems to have abandoned the linter. Replaced by "unused". # - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90) + +issues: + max-same-issues: 25 + uniq-by-line: false + +# TODO: enable this when we have coverage on docstring comments +# # The list of ids of default excludes to include or disable. +# include: +# - EXC0002 # disable excluding of issues about comments from golint + +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/README.md b/README.md index e1073c764d1..4a6453e191b 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ docker run --rm \ $(ImageName):$(ImageTag) ``` -### Supported sources +## Supported sources Grype can scan a variety of sources beyond those found in Docker. @@ -210,11 +210,54 @@ use the `--distro :` flag. A full example is: grype --add-cpes-if-none --distro alpine:3.10 sbom:some-alpine-3.10.spdx.json ``` +## Threat & Risk Prioritization + +This section explains the columns and UI cues that help prioritize remediation efforts: + +- **Severity**: String severity based on CVSS scores and indicate the significance of a vulnerability in levels. + This balances concerns such as ease of exploitability, and the potential to affect + confidentiality, integrity, and availability of software and services. + +- **EPSS**: + [Exploit Prediction Scoring System](https://www.first.org/epss/model) is a metric expressing the likelihood + that a vulnerability will be + exploited in the wild over the next 30 days (on a 0–1 scale); higher values signal a greater likelihood of + exploitation. + The table output shows the EPSS percentile, a one-way transform of the EPSS score showing the + proportion of all scored vulnerabilities with an equal or lower probability. + Percentiles linearize a heavily skewed distribution, making threshold choice (e.g. “only CVEs above the + 90th percentile”) straightforward. + +- **KEV Indicator**: Flags entries from CISA’s [Known Exploited Vulnerabilities Catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) + --an authoritative list of flaws observed being exploited in the wild. + +- **Risk Score**: A composite 0–100 metric calculated as: + ```markdown + risk = min(1, threat * average(severity)) * 100 + ``` + Where: + - `severity` is the average of all CVSS scores and string severity for a vulnerability (scaled between 0–1). + - `threat` is the EPSS score (between 0–1). If the vulnerability is on the KEV list then `threat` is + `1.05`, or `1.1` if the vulnerability is associated with a ransomware campaign. + This metric is one way to combine EPSS and CVSS suggested in the [EPSS user guide](https://www.first.org/epss/user-guide). + +- **Suggested Fixes**: All possible fixes for a package are listed, however, when multiple fixes are available, we de-emphasize all + upgrade paths except for the minimal upgrade path (which highlights the smallest, safest version bump). + +Results default to sorting by Risk Score and can be overridden with `--sort-by `: + +- `severity`: sort by severity +- `epss`: sort by EPSS percentile (aka, "threat") +- `risk`: sort by risk score +- `kev`: just like risk, except that KEV entries are always above non-KEV entries +- `package`: sort by package name, version, type +- `vulnerability`: sort by vulnerability ID + ### Supported versions Software updates are always applied to the latest version of Grype; fixes are not backported to any previous versions of Grype. -In terms of database updates, any version of Grype before v0.51.0 (Oct 2022, before schema v5) will not receive +In terms of database updates, any version of Grype before v0.51.0 (Oct 2022, before schema v5) will not receive vulnerability database updates. You can still build vulnerability databases for unsupported Grype releases by using previous releases of [vunnel](https://github.com/anchore/vunnel) to gather the upstream data and [grype-db](https://github.com/anchore/grype-db) to build databases for unsupported schemas. @@ -353,6 +396,8 @@ For example, here's how you could trigger a CI pipeline failure if any vulnerabi grype ubuntu:latest --fail-on medium ``` +**Note:** Grype returns exit code `2` on vulnerability errors. + ### Specifying matches to ignore If you're seeing Grype report **false positives** or any other vulnerability matches that you just don't want to see, you can tell Grype to **ignore** matches by specifying one or more _"ignore rules"_ in your Grype configuration file (e.g. `~/.grype.yaml`). This causes Grype not to report any vulnerability matches that meet the criteria specified by any of your ignore rules. @@ -697,192 +742,240 @@ GRYPE_CONFIG=/path/to/config.yaml grype Configuration options (example values are the default): ```yaml -# enable/disable checking for application updates on startup -# same as GRYPE_CHECK_FOR_APP_UPDATE env var +# the output format of the vulnerability report (options: table, template, json, cyclonedx) +# when using template as the output type, you must also provide a value for 'output-template-file' (env: GRYPE_OUTPUT) +output: 'table' + +# if using template output, you must provide a path to a Go template file +# see https://github.com/anchore/grype#using-templates for more information on template output +# the default path to the template file is the current working directory +# output-template-file: .grype/html.tmpl +# +# write output report to a file (default is to write to stdout) (env: GRYPE_FILE) +file: '' + +# pretty-print JSON output (env: GRYPE_PRETTY) +pretty: false + +# distro to match against in the format: : (env: GRYPE_DISTRO) +distro: '' + +# generate CPEs for packages with no CPE data (env: GRYPE_ADD_CPES_IF_NONE) +add-cpes-if-none: false + +# specify the path to a Go template file (requires 'template' output to be selected) (env: GRYPE_OUTPUT_TEMPLATE_FILE) +output-template-file: '' + +# enable/disable checking for application updates on startup (env: GRYPE_CHECK_FOR_APP_UPDATE) check-for-app-update: true -# allows users to specify which image source should be used to generate the sbom -# valid values are: registry, docker, podman -# same as GRYPE_DEFAULT_IMAGE_PULL_SOURCE env var -default-image-pull-source: "" +# ignore matches for vulnerabilities that are not fixed (env: GRYPE_ONLY_FIXED) +only-fixed: false -# same as --name; set the name of the target being analyzed -name: "" +# ignore matches for vulnerabilities that are fixed (env: GRYPE_ONLY_NOTFIXED) +only-notfixed: false + +# ignore matches for vulnerabilities with specified comma separated fix states, options=[fixed not-fixed unknown wont-fix] (env: GRYPE_IGNORE_WONTFIX) +ignore-wontfix: '' + +# an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux') (env: GRYPE_PLATFORM) +platform: '' # upon scanning, if a severity is found at or above the given severity then the return code will be 1 -# default is unset which will skip this validation (options: negligible, low, medium, high, critical) -# same as --fail-on ; GRYPE_FAIL_ON_SEVERITY env var -fail-on-severity: "" +# default is unset which will skip this validation (options: negligible, low, medium, high, critical) (env: GRYPE_FAIL_ON_SEVERITY) +fail-on-severity: '' -# the output format of the vulnerability report (options: table, template, json, cyclonedx) -# when using template as the output type, you must also provide a value for 'output-template-file' -# same as -o ; GRYPE_OUTPUT env var -output: "table" +# show suppressed/ignored vulnerabilities in the output (only supported with table output format) (env: GRYPE_SHOW_SUPPRESSED) +show-suppressed: false -# if using template output, you must provide a path to a Go template file -# see https://github.com/anchore/grype#using-templates for more information on template output -# the default path to the template file is the current working directory -# output-template-file: .grype/html.tmpl +# orient results by CVE instead of the original vulnerability ID when possible (env: GRYPE_BY_CVE) +by-cve: false -# write output report to a file (default is to write to stdout) -# same as --file; GRYPE_FILE env var -file: "" +# sort the match results with the given strategy, options=[package severity epss risk kev vulnerability] (env: GRYPE_SORT_BY) +sort-by: 'risk' -# a list of globs to exclude from scanning, for example: -# exclude: -# - '/etc/**' -# - './out/**/*.json' -# same as --exclude ; GRYPE_EXCLUDE env var -exclude: [] +# same as --name; set the name of the target being analyzed (env: GRYPE_NAME) +name: '' -# include matches on kernel-headers packages that are matched against upstream kernel package -# if 'false' any such matches are marked as ignored -match-upstream-kernel-headers: false +# allows users to specify which image source should be used to generate the sbom +# valid values are: registry, docker, podman (env: GRYPE_DEFAULT_IMAGE_PULL_SOURCE) +default-image-pull-source: '' -# os and/or architecture to use when referencing container images (e.g. "windows/armv6" or "arm64") -# same as --platform; GRYPE_PLATFORM env var -platform: "" +search: + # selection of layers to analyze, options=[squashed all-layers] (env: GRYPE_SEARCH_SCOPE) + scope: 'squashed' -# If using SBOM input, automatically generate CPEs when packages have none -add-cpes-if-none: false + # search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc) + # note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed + # note: for now this only applies to the java package cataloger (env: GRYPE_SEARCH_UNINDEXED_ARCHIVES) + unindexed-archives: false + + # search within archives that do contain a file index to search against (zip) + # note: for now this only applies to the java package cataloger (env: GRYPE_SEARCH_INDEXED_ARCHIVES) + indexed-archives: true + +# A list of vulnerability ignore rules, one or more property may be specified and all matching vulnerabilities will be ignored. +# This is the full set of supported rule fields: +# - vulnerability: CVE-2008-4318 +# fix-state: unknown +# package: +# name: libcurl +# version: 1.5.1 +# type: npm +# location: "/usr/local/lib/node_modules/**" +# +# VEX fields apply when Grype reads vex data: +# - vex-status: not_affected +# vex-justification: vulnerable_code_not_present +ignore: [] -# Explicitly specify a linux distribution to use as : like alpine:3.10 -distro: +# a list of globs to exclude from scanning, for example: +# - '/etc/**' +# - './out/**/*.json' +# same as --exclude (env: GRYPE_EXCLUDE) +exclude: [] external-sources: + # enable Grype searching network source for additional information (env: GRYPE_EXTERNAL_SOURCES_ENABLE) enable: false + maven: - search-upstream-by-sha1: true - base-url: https://search.maven.org/solrsearch/select + # search for Maven artifacts by SHA1 (env: GRYPE_EXTERNAL_SOURCES_MAVEN_SEARCH_MAVEN_UPSTREAM) + search-maven-upstream: true + + # base URL of the Maven repository to search (env: GRYPE_EXTERNAL_SOURCES_MAVEN_BASE_URL) + base-url: 'https://search.maven.org/solrsearch/select' + + # (env: GRYPE_EXTERNAL_SOURCES_MAVEN_RATE_LIMIT) rate-limit: 300ms -db: - # check for database updates on execution - # same as GRYPE_DB_AUTO_UPDATE env var - auto-update: true +match: + java: + # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_JAVA_USING_CPES) + using-cpes: false + + jvm: + # (env: GRYPE_MATCH_JVM_USING_CPES) + using-cpes: true - # location to write the vulnerability database cache; defaults to $XDG_CACHE_HOME/grype/db - # same as GRYPE_DB_CACHE_DIR env var - cache-dir: "" + dotnet: + # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_DOTNET_USING_CPES) + using-cpes: false - # URL of the vulnerability database - # same as GRYPE_DB_UPDATE_URL env var - update-url: "https://grype.anchore.io/databases" + golang: + # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_GOLANG_USING_CPES) + using-cpes: false - # it ensures db build is no older than the max-allowed-built-age - # set to false to disable check - validate-age: true + # use CPE matching to find vulnerabilities for the Go standard library (env: GRYPE_MATCH_GOLANG_ALWAYS_USE_CPE_FOR_STDLIB) + always-use-cpe-for-stdlib: true - # Max allowed age for vulnerability database, - # age being the time since it was built - # Default max age is 120h (or five days) - max-allowed-built-age: "120h" + # allow comparison between main module pseudo-versions (e.g. v0.0.0-20240413-2b432cf643...) (env: GRYPE_MATCH_GOLANG_ALLOW_MAIN_MODULE_PSEUDO_VERSION_COMPARISON) + allow-main-module-pseudo-version-comparison: false - # Timeout for downloading GRYPE_DB_UPDATE_URL to see if the database needs to be downloaded - # This file is ~156KiB as of 2024-04-17 so the download should be quick; adjust as needed - update-available-timeout: "30s" + javascript: + # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_JAVASCRIPT_USING_CPES) + using-cpes: false - # Timeout for downloading actual vulnerability DB - # The DB is ~156MB as of 2024-04-17 so slower connections may exceed the default timeout; adjust as needed - update-download-timeout: "120s" + python: + # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_PYTHON_USING_CPES) + using-cpes: false -search: - # the search space to look for packages (options: all-layers, squashed) - # same as -s ; GRYPE_SEARCH_SCOPE env var - scope: "squashed" + ruby: + # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_RUBY_USING_CPES) + using-cpes: false - # search within archives that do contain a file index to search against (zip) - # note: for now this only applies to the java package cataloger - # same as GRYPE_PACKAGE_SEARCH_INDEXED_ARCHIVES env var - indexed-archives: true + rust: + # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_RUST_USING_CPES) + using-cpes: false + + stock: + # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_STOCK_USING_CPES) + using-cpes: true - # search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc) - # note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed - # note: for now this only applies to the java package cataloger - # same as GRYPE_PACKAGE_SEARCH_UNINDEXED_ARCHIVES env var - unindexed-archives: false -# options when pulling directly from a registry via the "registry:" scheme registry: - # skip TLS verification when communicating with the registry - # same as GRYPE_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var + # skip TLS verification when communicating with the registry (env: GRYPE_REGISTRY_INSECURE_SKIP_TLS_VERIFY) insecure-skip-tls-verify: false - # use http instead of https when connecting to the registry - # same as GRYPE_REGISTRY_INSECURE_USE_HTTP env var + # use http instead of https when connecting to the registry (env: GRYPE_REGISTRY_INSECURE_USE_HTTP) insecure-use-http: false - # filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate - # GRYPE_REGISTRY_CA_CERT env var - ca-cert: "" + # Authentication credentials for specific registries. Each entry describes authentication for a specific authority: + # - authority: the registry authority URL the URL to the registry (e.g. "docker.io", "localhost:5000", etc.) (env: SYFT_REGISTRY_AUTH_AUTHORITY) + # username: a username if using basic credentials (env: SYFT_REGISTRY_AUTH_USERNAME) + # password: a corresponding password (env: SYFT_REGISTRY_AUTH_PASSWORD) + # token: a token if using token-based authentication, mutually exclusive with username/password (env: SYFT_REGISTRY_AUTH_TOKEN) + # tls-cert: filepath to the client certificate used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_CERT) + # tls-key: filepath to the client key used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_KEY) + auth: [] + + # filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate (env: GRYPE_REGISTRY_CA_CERT) + ca-cert: '' + +# a list of VEX documents to consider when producing scanning results (env: GRYPE_VEX_DOCUMENTS) +vex-documents: [] - # credentials for specific registries - auth: - # the URL to the registry (e.g. "docker.io", "localhost:5000", etc.) - # GRYPE_REGISTRY_AUTH_AUTHORITY env var - - authority: "" +# VEX statuses to consider as ignored rules (env: GRYPE_VEX_ADD) +vex-add: [] - # GRYPE_REGISTRY_AUTH_USERNAME env var - username: "" +# match kernel-header packages with upstream kernel as kernel vulnerabilities (env: GRYPE_MATCH_UPSTREAM_KERNEL_HEADERS) +match-upstream-kernel-headers: false + +db: + # location to write the vulnerability database cache (env: GRYPE_DB_CACHE_DIR) + cache-dir: '~/Library/Caches/grype/db' + + # URL of the vulnerability database (env: GRYPE_DB_UPDATE_URL) + update-url: 'https://grype.anchore.io/databases' - # GRYPE_REGISTRY_AUTH_PASSWORD env var - password: "" + # certificate to trust download the database and listing file (env: GRYPE_DB_CA_CERT) + ca-cert: '' - # note: token and username/password are mutually exclusive - # GRYPE_REGISTRY_AUTH_TOKEN env var - token: "" + # check for database updates on execution (env: GRYPE_DB_AUTO_UPDATE) + auto-update: true - # filepath to the client certificate used for TLS authentication to the registry - # GRYPE_REGISTRY_AUTH_TLS_CERT env var - tls-cert: "" + # validate the database matches the known hash each execution (env: GRYPE_DB_VALIDATE_BY_HASH_ON_START) + validate-by-hash-on-start: true - # filepath to the client key used for TLS authentication to the registry - # GRYPE_REGISTRY_AUTH_TLS_KEY env var - tls-key: "" + # ensure db build is no older than the max-allowed-built-age (env: GRYPE_DB_VALIDATE_AGE) + validate-age: true - # - ... # note, more credentials can be provided via config file only (not env vars) + # Max allowed age for vulnerability database, + # age being the time since it was built + # Default max age is 120h (or five days) (env: GRYPE_DB_MAX_ALLOWED_BUILT_AGE) + max-allowed-built-age: 120h0m0s + # fail the scan if unable to check for database updates (env: GRYPE_DB_REQUIRE_UPDATE_CHECK) + require-update-check: false + + # Timeout for downloading GRYPE_DB_UPDATE_URL to see if the database needs to be downloaded + # This file is ~156KiB as of 2024-04-17 so the download should be quick; adjust as needed (env: GRYPE_DB_UPDATE_AVAILABLE_TIMEOUT) + update-available-timeout: 30s + + # Timeout for downloading actual vulnerability DB + # The DB is ~156MB as of 2024-04-17 so slower connections may exceed the default timeout; adjust as needed (env: GRYPE_DB_UPDATE_DOWNLOAD_TIMEOUT) + update-download-timeout: 5m0s + + # Maximum frequency to check for vulnerability database updates (env: GRYPE_DB_MAX_UPDATE_CHECK_FREQUENCY) + max-update-check-frequency: 2h0m0s log: - # suppress all output (except for the vulnerability list) - # same as -q ; GRYPE_LOG_QUIET env var + # suppress all logging output (env: GRYPE_LOG_QUIET) quiet: false - # increase verbosity - # same as GRYPE_LOG_VERBOSITY env var - verbosity: 0 + # explicitly set the logging level (available: [error warn info debug trace]) (env: GRYPE_LOG_LEVEL) + level: 'warn' - # the log level; note: detailed logging suppress the ETUI - # same as GRYPE_LOG_LEVEL env var - # Uses logrus logging levels: https://github.com/sirupsen/logrus#level-logging - level: "error" + # file path to write logs to (env: GRYPE_LOG_FILE) + file: '' - # location to write the log file (default is not to have a log file) - # same as GRYPE_LOG_FILE env var - file: "" +dev: + # capture resource profiling data (available: [cpu, mem]) (env: GRYPE_DEV_PROFILE) + profile: '' -match: - # sets the matchers below to use cpes when trying to find - # vulnerability matches. The stock matcher is the default - # when no primary matcher can be identified. - java: - using-cpes: false - python: - using-cpes: false - javascript: - using-cpes: false - ruby: - using-cpes: false - dotnet: - using-cpes: false - golang: - using-cpes: false - # even if CPE matching is disabled, make an exception when scanning for "stdlib". - always-use-cpe-for-stdlib: true - # allow main module pseudo versions, which may have only been "guessed at" by Syft, to be used in vulnerability matching - allow-main-module-pseudo-version-comparison: false - stock: - using-cpes: true + db: + # show sql queries in trace logging (requires -vv) (env: GRYPE_DEV_DB_DEBUG) + debug: false ``` ## Future plans diff --git a/cmd/grype/cli/cli.go b/cmd/grype/cli/cli.go index f19bbb2ef99..76ddf5a54e3 100644 --- a/cmd/grype/cli/cli.go +++ b/cmd/grype/cli/cli.go @@ -1,9 +1,13 @@ package cli import ( + "errors" "os" "runtime/debug" + "strings" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" "github.com/spf13/cobra" "github.com/anchore/clio" @@ -11,6 +15,7 @@ import ( grypeHandler "github.com/anchore/grype/cmd/grype/cli/ui" "github.com/anchore/grype/cmd/grype/internal/ui" v6 "github.com/anchore/grype/grype/db/v6" + "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/redact" @@ -37,6 +42,10 @@ func create(id clio.Identification) (clio.Application, *cobra.Command) { WithUIConstructor( // select a UI based on the logging configuration and state of stdin (if stdin is a tty) func(cfg clio.Config) (*clio.UICollection, error) { + // remove CI var from consideration when determining if we should use the UI + lipgloss.SetDefaultRenderer(lipgloss.NewRenderer(os.Stdout, termenv.WithEnvironment(environWithoutCI{}))) + + // setup the UIs noUI := ui.None(cfg.Log.Quiet) if !cfg.Log.AllowUI(os.Stdin) || cfg.Log.Quiet { return clio.NewUICollection(noUI), nil @@ -70,6 +79,18 @@ func create(id clio.Identification) (clio.Application, *cobra.Command) { ). WithPostRuns(func(_ *clio.State, _ error) { stereoscope.Cleanup() + }). + WithMapExitCode(func(err error) int { + // return exit code 2 to indicate when a vulnerability severity is discovered + // that is equal or above the given --fail-on severity value. + if errors.Is(err, grypeerr.ErrAboveSeverityThreshold) { + return 2 + } + // return exit code 100 to indicate a DB upgrade is available (cmd: db check). + if errors.Is(err, grypeerr.ErrDBUpgradeAvailable) { + return 100 + } + return 1 }) app := clio.New(*clioCfg) @@ -108,3 +129,24 @@ func syftVersion() (string, any) { func dbVersion() (string, any) { return "Supported DB Schema", v6.ModelVersion } + +type environWithoutCI struct { +} + +func (e environWithoutCI) Environ() []string { + var out []string + for _, s := range os.Environ() { + if strings.HasPrefix(s, "CI=") { + continue + } + out = append(out, s) + } + return out +} + +func (e environWithoutCI) Getenv(s string) string { + if s == "CI" { + return "" + } + return os.Getenv(s) +} diff --git a/cmd/grype/cli/commands/db_check.go b/cmd/grype/cli/commands/db_check.go index 18bc49b89e5..20d035636c5 100644 --- a/cmd/grype/cli/commands/db_check.go +++ b/cmd/grype/cli/commands/db_check.go @@ -12,13 +12,10 @@ import ( "github.com/anchore/grype/cmd/grype/cli/options" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" + "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/internal/log" ) -const ( - exitCodeOnDBUpgradeAvailable = 100 -) - type dbCheckOptions struct { Output string `yaml:"output" json:"output" mapstructure:"output"` options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` @@ -85,7 +82,7 @@ func runDBCheck(opts dbCheckOptions) error { } if updateAvailable { - os.Exit(exitCodeOnDBUpgradeAvailable) //nolint:gocritic + return grypeerr.ErrDBUpgradeAvailable } return nil } diff --git a/cmd/grype/cli/commands/db_import.go b/cmd/grype/cli/commands/db_import.go index 03beeb453a9..587ce08e281 100644 --- a/cmd/grype/cli/commands/db_import.go +++ b/cmd/grype/cli/commands/db_import.go @@ -16,9 +16,9 @@ func DBImport(app clio.Application) *cobra.Command { opts := options.DefaultDatabaseCommand(app.ID()) cmd := &cobra.Command{ - Use: "import FILE", - Short: "Import a vulnerability database or archive", - Long: fmt.Sprintf("import a vulnerability database or archive from a local FILE.\nDB archives can be obtained from %q.", opts.DB.UpdateURL), + Use: "import FILE | URL", + Short: "Import a vulnerability database or archive from a local file or URL", + Long: fmt.Sprintf("import a vulnerability database archive from a local FILE or URL.\nDB archives can be obtained from %q (or running `db list`). If the URL has a `checksum` query parameter with a fully qualified digest (e.g. 'sha256:abc728...') then the archive/DB will be verified against this value.", opts.DB.UpdateURL), Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { return runDBImport(*opts, args[0]) @@ -33,7 +33,7 @@ func DBImport(app clio.Application) *cobra.Command { return app.SetupCommand(cmd, &configWrapper{opts}) } -func runDBImport(opts options.DatabaseCommand, dbArchivePath string) error { +func runDBImport(opts options.DatabaseCommand, reference string) error { // TODO: tui update? better logging? client, err := distribution.NewClient(opts.ToClientConfig()) if err != nil { @@ -44,12 +44,12 @@ func runDBImport(opts options.DatabaseCommand, dbArchivePath string) error { return fmt.Errorf("unable to create curator: %w", err) } - log.WithFields("path", dbArchivePath).Infof("importing vulnerability database archive") - if err := c.Import(dbArchivePath); err != nil { + log.WithFields("reference", reference).Infof("importing vulnerability database archive") + if err := c.Import(reference); err != nil { return fmt.Errorf("unable to import vulnerability database: %w", err) } s := c.Status() - log.WithFields("built", s.Built.String(), "status", s.Status()).Info("vulnerability database imported") + log.WithFields("built", s.Built.String(), "status", renderStoreValidation(s)).Info("vulnerability database imported") return nil } diff --git a/cmd/grype/cli/commands/db_list.go b/cmd/grype/cli/commands/db_list.go index f20ecd84f6b..3be6a4a9d7b 100644 --- a/cmd/grype/cli/commands/db_list.go +++ b/cmd/grype/cli/commands/db_list.go @@ -6,7 +6,6 @@ import ( "io" "net/url" "os" - "path" "github.com/spf13/cobra" @@ -62,28 +61,39 @@ func runDBList(opts dbListOptions) error { return fmt.Errorf("unable to get database listing: %w", err) } - return presentDBList(opts.Output, opts.DB.UpdateURL, os.Stdout, latest) + u, err := c.ResolveArchiveURL(latest.Archive) + if err != nil { + return fmt.Errorf("unable to resolve database URL: %w", err) + } + + return presentDBList(opts.Output, u, opts.DB.UpdateURL, os.Stdout, latest) } -func presentDBList(format string, u string, writer io.Writer, latest *distribution.LatestDocument) error { +func presentDBList(format string, archiveURL, listingURL string, writer io.Writer, latest *distribution.LatestDocument) error { if latest == nil { return fmt.Errorf("no database listing found") } - parsedURL, err := url.Parse(u) + // remove query params + archiveURLObj, err := url.Parse(archiveURL) if err != nil { - return fmt.Errorf("failed to parse base URL: %w", err) + return fmt.Errorf("unable to parse db URL %q: %w", archiveURL, err) } - parsedURL.Path = path.Join(path.Dir(parsedURL.Path), latest.Path) + archiveURLObj.RawQuery = "" + + if listingURL == distribution.DefaultConfig().LatestURL { + // append on the schema + listingURL = fmt.Sprintf("%s/v%v/%s", listingURL, latest.SchemaVersion.Model, distribution.LatestFileName) + } switch format { case textOutputFormat: fmt.Fprintf(writer, "Status: %s\n", latest.Status) fmt.Fprintf(writer, "Schema: %s\n", latest.SchemaVersion.String()) fmt.Fprintf(writer, "Built: %s\n", latest.Built.String()) - fmt.Fprintf(writer, "Listing: %s\n", u) - fmt.Fprintf(writer, "DB URL: %s\n", parsedURL.String()) + fmt.Fprintf(writer, "Listing: %s\n", listingURL) + fmt.Fprintf(writer, "DB URL: %s\n", archiveURLObj.String()) fmt.Fprintf(writer, "Checksum: %s\n", latest.Checksum) case jsonOutputFormat, "raw": enc := json.NewEncoder(writer) diff --git a/cmd/grype/cli/commands/db_list_test.go b/cmd/grype/cli/commands/db_list_test.go index 30d24194c49..3492f8e4114 100644 --- a/cmd/grype/cli/commands/db_list_test.go +++ b/cmd/grype/cli/commands/db_list_test.go @@ -66,7 +66,6 @@ func Test_ListingUserAgent(t *testing.T) { } func TestPresentDBList(t *testing.T) { - baseURL := "http://localhost:8000/latest.json" latestDoc := &distribution.LatestDocument{ Status: "active", Archive: distribution.Archive{ @@ -82,20 +81,39 @@ func TestPresentDBList(t *testing.T) { tests := []struct { name string format string + baseURL string + archiveURL string latest *distribution.LatestDocument expectedText string expectedErr require.ErrorAssertionFunc }{ { - name: "valid text format", - format: textOutputFormat, - latest: latestDoc, + name: "valid text format", + format: textOutputFormat, + latest: latestDoc, + baseURL: "http://localhost:8000/latest.json", + archiveURL: "http://localhost:8000/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst", expectedText: `Status: active Schema: v6.0.0 Built: 2024-11-27T14:43:17Z Listing: http://localhost:8000/latest.json DB URL: http://localhost:8000/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst Checksum: sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4 +`, + expectedErr: require.NoError, + }, + { + name: "complete default values", + format: textOutputFormat, + latest: latestDoc, + baseURL: "https://grype.anchore.io/databases", + archiveURL: "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst", + expectedText: `Status: active +Schema: v6.0.0 +Built: 2024-11-27T14:43:17Z +Listing: https://grype.anchore.io/databases/v6/latest.json +DB URL: https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst +Checksum: sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4 `, expectedErr: require.NoError, }, @@ -133,7 +151,7 @@ Checksum: sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca t.Run(tt.name, func(t *testing.T) { writer := &bytes.Buffer{} - err := presentDBList(tt.format, baseURL, writer, tt.latest) + err := presentDBList(tt.format, tt.archiveURL, tt.baseURL, writer, tt.latest) if tt.expectedErr == nil { tt.expectedErr = require.NoError } diff --git a/cmd/grype/cli/commands/db_search_vuln.go b/cmd/grype/cli/commands/db_search_vuln.go index f9ea918be39..22847c049fe 100644 --- a/cmd/grype/cli/commands/db_search_vuln.go +++ b/cmd/grype/cli/commands/db_search_vuln.go @@ -6,6 +6,7 @@ import ( "io" "sort" "strings" + "time" "github.com/hashicorp/go-multierror" "github.com/scylladb/go-set/strset" @@ -17,7 +18,9 @@ import ( v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" + "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/bus" + "github.com/anchore/grype/internal/cvss" ) type dbSearchVulnerabilityOptions struct { @@ -166,36 +169,14 @@ func renderDBSearchVulnerabilitiesTableRows(structuredRows []dbsearch.Vulnerabil versionsByRow := make(map[row][]string) for _, rr := range structuredRows { - // get the first severity value (which is ranked highest) - var sev string - if len(rr.Severities) > 0 { - sev = fmt.Sprintf("%s", rr.Severities[0].Value) - } - - prov := rr.Provider - var versions []string - for _, os := range rr.OperatingSystems { - versions = append(versions, os.Version) - } - - var published string - if rr.PublishedDate != nil && !rr.PublishedDate.IsZero() { - published = rr.PublishedDate.Format("2006-01-02") - } - - var ref string - if len(rr.References) > 0 { - ref = rr.References[0].URL - } - r := row{ Vuln: rr.ID, - ProviderWithoutVersions: prov, - PublishedDate: published, - Severity: sev, - Reference: ref, + ProviderWithoutVersions: rr.Provider, + PublishedDate: getDate(rr.PublishedDate), + Severity: getSeverity(rr.Severities), + Reference: getPrimaryReference(rr.References), } - versionsByRow[r] = append(versionsByRow[r], versions...) + versionsByRow[r] = append(versionsByRow[r], getOSVersions(rr.OperatingSystems)...) } var rows [][]string @@ -220,3 +201,41 @@ func renderDBSearchVulnerabilitiesTableRows(structuredRows []dbsearch.Vulnerabil return rows } + +func getOSVersions(oss []dbsearch.OperatingSystem) []string { + var versions []string + for _, os := range oss { + versions = append(versions, os.Version) + } + return versions +} + +func getPrimaryReference(refs []v6.Reference) string { + if len(refs) > 0 { + return refs[0].URL + } + + return "" +} + +func getDate(t *time.Time) string { + if t != nil && !t.IsZero() { + return t.Format("2006-01-02") + } + return "" +} + +func getSeverity(sevs []v6.Severity) string { + if len(sevs) == 0 { + return vulnerability.UnknownSeverity.String() + } + // get the first severity value (which is ranked highest) + switch v := sevs[0].Value.(type) { + case string: + return v + case dbsearch.CVSSSeverity: + return cvss.SeverityFromBaseScore(v.Metrics.BaseScore).String() + } + + return fmt.Sprintf("%v", sevs[0].Value) +} diff --git a/cmd/grype/cli/commands/db_search_vuln_test.go b/cmd/grype/cli/commands/db_search_vuln_test.go new file mode 100644 index 00000000000..742024438e7 --- /dev/null +++ b/cmd/grype/cli/commands/db_search_vuln_test.go @@ -0,0 +1,228 @@ +package commands + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch" + v6 "github.com/anchore/grype/grype/db/v6" + "github.com/anchore/grype/grype/vulnerability" +) + +func TestGetOSVersions(t *testing.T) { + tests := []struct { + name string + input []dbsearch.OperatingSystem + expected []string + }{ + { + name: "empty list", + input: []dbsearch.OperatingSystem{}, + expected: nil, + }, + { + name: "single os", + input: []dbsearch.OperatingSystem{ + { + Name: "debian", + Version: "11", + }, + }, + expected: []string{"11"}, + }, + { + name: "multiple os", + input: []dbsearch.OperatingSystem{ + { + Name: "ubuntu", + Version: "16.04", + }, + { + Name: "ubuntu", + Version: "22.04", + }, + { + Name: "ubuntu", + Version: "24.04", + }, + }, + expected: []string{"16.04", "22.04", "24.04"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := getOSVersions(tt.input) + require.Equal(t, tt.expected, actual) + }) + } +} + +func TestGetPrimaryReference(t *testing.T) { + tests := []struct { + name string + input []v6.Reference + expected string + }{ + { + name: "empty list", + input: []v6.Reference{}, + expected: "", + }, + { + name: "single reference", + input: []v6.Reference{ + { + URL: "https://example.com/vuln/123", + Tags: []string{"primary"}, + }, + }, + expected: "https://example.com/vuln/123", + }, + { + name: "multiple references", + input: []v6.Reference{ + { + URL: "https://example.com/vuln/123", + Tags: []string{"primary"}, + }, + { + URL: "https://example.com/advisory/123", + Tags: []string{"secondary"}, + }, + }, + expected: "https://example.com/vuln/123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := getPrimaryReference(tt.input) + require.Equal(t, tt.expected, actual) + }) + } +} + +func TestGetDate(t *testing.T) { + tests := []struct { + name string + input *time.Time + expected string + }{ + { + name: "nil time", + input: nil, + expected: "", + }, + { + name: "zero time", + input: &time.Time{}, + expected: "", + }, + { + name: "valid time", + input: timePtr(time.Date(2023, 5, 15, 0, 0, 0, 0, time.UTC)), + expected: "2023-05-15", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := getDate(tt.input) + require.Equal(t, tt.expected, actual) + }) + } +} + +func TestGetSeverity(t *testing.T) { + tests := []struct { + name string + input []v6.Severity + expected string + }{ + { + name: "empty list", + input: []v6.Severity{}, + expected: vulnerability.UnknownSeverity.String(), + }, + { + name: "string severity", + input: []v6.Severity{ + { + Scheme: "HML", + Value: "high", + Source: "nvd@nist.gov", + Rank: 1, + }, + }, + expected: "high", + }, + { + name: "CVSS severity", + input: []v6.Severity{ + { + Scheme: "CVSS_V3", + Value: dbsearch.CVSSSeverity{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + Version: "3.1", + Metrics: dbsearch.CvssMetrics{ + BaseScore: 9.8, + }, + }, + Source: "nvd@nist.gov", + Rank: 1, + }, + }, + expected: "critical", + }, + { + name: "other value type", + input: []v6.Severity{ + { + Scheme: "OTHER", + Value: 42.0, + Source: "custom", + Rank: 1, + }, + }, + expected: "42", + }, + { + name: "multiple severities", + input: []v6.Severity{ + { + Scheme: "HML", + Value: "high", + Source: "nvd@nist.gov", + Rank: 1, + }, + { + Scheme: "CVSS_V3", + Value: dbsearch.CVSSSeverity{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + Version: "3.1", + Metrics: dbsearch.CvssMetrics{ + BaseScore: 9.8, + }, + }, + Source: "nvd@nist.gov", + Rank: 2, + }, + }, + expected: "high", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := getSeverity(tt.input) + require.Equal(t, tt.expected, actual) + }) + } +} + +func timePtr(t time.Time) *time.Time { + return &t +} diff --git a/cmd/grype/cli/commands/db_status.go b/cmd/grype/cli/commands/db_status.go index 46c31c63f10..af402e27aab 100644 --- a/cmd/grype/cli/commands/db_status.go +++ b/cmd/grype/cli/commands/db_status.go @@ -5,14 +5,15 @@ import ( "fmt" "io" "os" + "time" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" - v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" + "github.com/anchore/grype/grype/vulnerability" ) type dbStatusOptions struct { @@ -67,17 +68,19 @@ func runDBStatus(opts dbStatusOptions) error { return fmt.Errorf("failed to present db status information: %+v", err) } - return status.Err + return status.Error } -func presentDBStatus(format string, writer io.Writer, status v6.Status) error { +func presentDBStatus(format string, writer io.Writer, status vulnerability.ProviderStatus) error { switch format { case textOutputFormat: fmt.Fprintln(writer, "Path: ", status.Path) fmt.Fprintln(writer, "Schema: ", status.SchemaVersion) - fmt.Fprintln(writer, "Built: ", status.Built.String()) - fmt.Fprintln(writer, "Checksum: ", status.Checksum) - fmt.Fprintln(writer, "Status: ", status.Status()) + fmt.Fprintln(writer, "Built: ", status.Built.Format(time.RFC3339)) + if status.From != "" { + fmt.Fprintln(writer, "From: ", status.From) + } + fmt.Fprintln(writer, "Status: ", renderStoreValidation(status)) case jsonOutputFormat: enc := json.NewEncoder(writer) enc.SetEscapeHTML(false) @@ -91,3 +94,10 @@ func presentDBStatus(format string, writer io.Writer, status v6.Status) error { return nil } + +func renderStoreValidation(status vulnerability.ProviderStatus) string { + if status.Error != nil { + return "invalid" + } + return "valid" +} diff --git a/cmd/grype/cli/commands/db_status_test.go b/cmd/grype/cli/commands/db_status_test.go index dbfbcc73525..a2d774a8b4c 100644 --- a/cmd/grype/cli/commands/db_status_test.go +++ b/cmd/grype/cli/commands/db_status_test.go @@ -10,30 +10,30 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - v6 "github.com/anchore/grype/grype/db/v6" + "github.com/anchore/grype/grype/vulnerability" ) func TestPresentDBStatus(t *testing.T) { - validStatus := v6.Status{ + validStatus := vulnerability.ProviderStatus{ Path: "/Users/test/Library/Caches/grype/db/6/vulnerability.db", + From: "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8", SchemaVersion: "6.0.0", - Built: v6.Time{Time: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC)}, - Checksum: "xxh64:89d3ae128f6e718e", - Err: nil, + Built: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC), + Error: nil, } - invalidStatus := v6.Status{ + invalidStatus := vulnerability.ProviderStatus{ Path: "/Users/test/Library/Caches/grype/db/6/vulnerability.db", + From: "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8", SchemaVersion: "6.0.0", - Built: v6.Time{Time: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC)}, - Checksum: "xxh64:89d3ae128f6e718e", - Err: errors.New("checksum mismatch"), + Built: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC), + Error: errors.New("checksum mismatch"), } tests := []struct { name string format string - status v6.Status + status vulnerability.ProviderStatus expectedText string expectedErr require.ErrorAssertionFunc }{ @@ -44,7 +44,7 @@ func TestPresentDBStatus(t *testing.T) { expectedText: `Path: /Users/test/Library/Caches/grype/db/6/vulnerability.db Schema: 6.0.0 Built: 2024-11-27T14:43:17Z -Checksum: xxh64:89d3ae128f6e718e +From: https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8 Status: valid `, expectedErr: require.NoError, @@ -56,7 +56,7 @@ Status: valid expectedText: `Path: /Users/test/Library/Caches/grype/db/6/vulnerability.db Schema: 6.0.0 Built: 2024-11-27T14:43:17Z -Checksum: xxh64:89d3ae128f6e718e +From: https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8 Status: invalid `, expectedErr: require.NoError, @@ -67,10 +67,10 @@ Status: invalid status: validStatus, expectedText: `{ "schemaVersion": "6.0.0", + "from": "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8", "built": "2024-11-27T14:43:17Z", "path": "/Users/test/Library/Caches/grype/db/6/vulnerability.db", - "checksum": "xxh64:89d3ae128f6e718e", - "error": "" + "valid": true } `, expectedErr: require.NoError, @@ -81,9 +81,10 @@ Status: invalid status: invalidStatus, expectedText: `{ "schemaVersion": "6.0.0", + "from": "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8", "built": "2024-11-27T14:43:17Z", "path": "/Users/test/Library/Caches/grype/db/6/vulnerability.db", - "checksum": "xxh64:89d3ae128f6e718e", + "valid": false, "error": "checksum mismatch" } `, diff --git a/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go b/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go index cc5190dd0e7..17af2ba64d7 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go +++ b/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go @@ -33,6 +33,10 @@ type AffectedPackageInfo struct { // CPE is a Common Platform Enumeration that is affected by the vulnerability CPE *CPE `json:"cpe,omitempty"` + // Namespace is a holdover value from the v5 DB schema that combines provider and search methods into a single value + // Deprecated: this field will be removed in a later version of the search schema + Namespace string `json:"namespace"` + // Detail is the detailed information about the affected package Detail v6.AffectedPackageBlob `json:"detail"` } @@ -110,10 +114,11 @@ func newAffectedPackageRows(affectedPkgs []affectedPackageWithDecorations, affec rows = append(rows, AffectedPackage{ Vulnerability: newVulnerabilityInfo(*pkg.Vulnerability, pkg.vulnerabilityDecorations), AffectedPackageInfo: AffectedPackageInfo{ - Model: &pkg.AffectedPackageHandle, - OS: toOS(pkg.OperatingSystem), - Package: toPackage(pkg.Package), - Detail: detail, + Model: &pkg.AffectedPackageHandle, + OS: toOS(pkg.OperatingSystem), + Package: toPackage(pkg.Package), + Namespace: v6.MimicV5Namespace(pkg.Vulnerability, &pkg.AffectedPackageHandle), + Detail: detail, }, }) } @@ -138,8 +143,9 @@ func newAffectedPackageRows(affectedPkgs []affectedPackageWithDecorations, affec // tracking model information is not possible with CPE handles Vulnerability: newVulnerabilityInfo(*ac.Vulnerability, ac.vulnerabilityDecorations), AffectedPackageInfo: AffectedPackageInfo{ - CPE: c, - Detail: detail, + CPE: c, + Namespace: v6.MimicV5Namespace(ac.Vulnerability, nil), // no affected package will default to NVD + Detail: detail, }, }) } diff --git a/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go b/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go index b67b32d2e3f..5c6442f9581 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go +++ b/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go @@ -49,8 +49,9 @@ func TestAffectedPackageTableRowMarshalJSON(t *testing.T) { }, }, AffectedPackageInfo: AffectedPackageInfo{ - Package: &Package{Name: "pkg1", Ecosystem: "ecosystem1"}, - CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"}, + Package: &Package{Name: "pkg1", Ecosystem: "ecosystem1"}, + CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"}, + Namespace: "namespace1", Detail: v6.AffectedPackageBlob{ CVEs: []string{"CVE-1234-5678"}, Qualifiers: &v6.AffectedPackageQualifiers{ @@ -120,6 +121,7 @@ func TestAffectedPackageTableRowMarshalJSON(t *testing.T) { "ecosystem": "ecosystem1" }, "cpe": "cpe:2.3:a:vendor1:product1:*:*:*:*:*:*", + "namespace": "namespace1", "detail": { "cves": [ "CVE-1234-5678" @@ -301,8 +303,9 @@ func TestNewAffectedPackageRows(t *testing.T) { }, }, AffectedPackageInfo: AffectedPackageInfo{ - OS: &OperatingSystem{Name: "Linux", Version: "5.10"}, - Package: &Package{Name: "pkg1", Ecosystem: "ecosystem1"}, + OS: &OperatingSystem{Name: "Linux", Version: "5.10"}, + Package: &Package{Name: "pkg1", Ecosystem: "ecosystem1"}, + Namespace: "provider1:distro:Linux:5.10", Detail: v6.AffectedPackageBlob{ CVEs: []string{"CVE-1234-5678"}, Qualifiers: &v6.AffectedPackageQualifiers{ @@ -352,7 +355,8 @@ func TestNewAffectedPackageRows(t *testing.T) { }, }, AffectedPackageInfo: AffectedPackageInfo{ - CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"}, + CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"}, + Namespace: "provider2:cpe", Detail: v6.AffectedPackageBlob{ CVEs: []string{"CVE-9876-5432"}, Ranges: []v6.AffectedRange{ @@ -535,8 +539,9 @@ func TestAffectedPackages(t *testing.T) { }, }, AffectedPackageInfo: AffectedPackageInfo{ - OS: &OperatingSystem{Name: "Linux", Version: "5.10"}, - Package: &Package{Name: "pkg1", Ecosystem: "ecosystem1"}, + OS: &OperatingSystem{Name: "Linux", Version: "5.10"}, + Package: &Package{Name: "pkg1", Ecosystem: "ecosystem1"}, + Namespace: "provider1:distro:Linux:5.10", Detail: v6.AffectedPackageBlob{ CVEs: []string{"CVE-1234-5678"}, Ranges: []v6.AffectedRange{ @@ -582,7 +587,8 @@ func TestAffectedPackages(t *testing.T) { }, }, AffectedPackageInfo: AffectedPackageInfo{ - CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"}, + CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"}, + Namespace: "provider2:cpe", Detail: v6.AffectedPackageBlob{ CVEs: []string{"CVE-9876-5432"}, Ranges: []v6.AffectedRange{ diff --git a/cmd/grype/cli/commands/internal/dbsearch/matches.go b/cmd/grype/cli/commands/internal/dbsearch/matches.go index 8343adb1107..0e3aba8399f 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/matches.go +++ b/cmd/grype/cli/commands/internal/dbsearch/matches.go @@ -41,7 +41,7 @@ func (m Matches) Flatten() []AffectedPackage { return rows } -func newMatchesRows(affectedPkgs []affectedPackageWithDecorations, affectedCPEs []affectedCPEWithDecorations) (rows []Match, retErr error) { +func newMatchesRows(affectedPkgs []affectedPackageWithDecorations, affectedCPEs []affectedCPEWithDecorations) (rows []Match, retErr error) { // nolint:funlen var affectedPkgsByVuln = make(map[v6.ID][]AffectedPackageInfo) var vulnsByID = make(map[v6.ID]v6.VulnerabilityHandle) var decorationsByID = make(map[v6.ID]vulnerabilityDecorations) @@ -62,10 +62,11 @@ func newMatchesRows(affectedPkgs []affectedPackageWithDecorations, affectedCPEs } aff := AffectedPackageInfo{ - Model: &pkg.AffectedPackageHandle, - OS: toOS(pkg.OperatingSystem), - Package: toPackage(pkg.Package), - Detail: detail, + Model: &pkg.AffectedPackageHandle, + OS: toOS(pkg.OperatingSystem), + Package: toPackage(pkg.Package), + Namespace: v6.MimicV5Namespace(pkg.Vulnerability, &pkg.AffectedPackageHandle), + Detail: detail, } affectedPkgsByVuln[pkg.Vulnerability.ID] = append(affectedPkgsByVuln[pkg.Vulnerability.ID], aff) @@ -94,8 +95,9 @@ func newMatchesRows(affectedPkgs []affectedPackageWithDecorations, affectedCPEs aff := AffectedPackageInfo{ // tracking model information is not possible with CPE handles - CPE: c, - Detail: detail, + CPE: c, + Namespace: v6.MimicV5Namespace(ac.Vulnerability, nil), // no affected package will default to NVD + Detail: detail, } affectedPkgsByVuln[ac.Vulnerability.ID] = append(affectedPkgsByVuln[ac.Vulnerability.ID], aff) diff --git a/cmd/grype/cli/commands/internal/dbsearch/versions.go b/cmd/grype/cli/commands/internal/dbsearch/versions.go index 292114f4b6e..fd795136133 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/versions.go +++ b/cmd/grype/cli/commands/internal/dbsearch/versions.go @@ -2,11 +2,12 @@ package dbsearch const ( // MatchesSchemaVersion is the schema version for the `db search ` command - MatchesSchemaVersion = "1.0.1" + MatchesSchemaVersion = "1.0.2" // MatchesSchemaVersion Changelog: // 1.0.0 - Initial schema 🎉 // 1.0.1 - Add KEV and EPSS data to vulnerability matches + // 1.0.2 - Add v5 namespace emulation for affected packages // VulnerabilitiesSchemaVersion is the schema version for the `db search vuln` command VulnerabilitiesSchemaVersion = "1.0.1" diff --git a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go index e53f30a7c39..6b4c7622af0 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go +++ b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go @@ -7,6 +7,7 @@ import ( "time" v6 "github.com/anchore/grype/grype/db/v6" + "github.com/anchore/grype/internal/cvss" "github.com/anchore/grype/internal/log" ) @@ -83,6 +84,23 @@ type EPSS struct { Date string `json:"date"` } +type CVSSSeverity struct { + // Vector is the CVSS assessment as a parameterized string + Vector string `json:"vector"` + + // Version is the CVSS version (e.g. "3.0") + Version string `json:"version,omitempty"` + + // Metrics is the CVSS quantitative assessment based on the vector + Metrics CvssMetrics `json:"metrics"` +} + +type CvssMetrics struct { + BaseScore float64 `json:"baseScore"` + ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` + ImpactScore *float64 `json:"impactScore,omitempty"` +} + type vulnerabilityAffectedPackageJoin struct { Vulnerability v6.VulnerabilityHandle OperatingSystems []v6.OperatingSystem @@ -111,6 +129,7 @@ func newVulnerabilityInfo(vuln v6.VulnerabilityHandle, vc vulnerabilityDecoratio if vuln.BlobValue != nil { blob = *vuln.BlobValue } + patchCVSSMetrics(&blob) return VulnerabilityInfo{ Model: vuln, VulnerabilityBlob: blob, @@ -124,6 +143,29 @@ func newVulnerabilityInfo(vuln v6.VulnerabilityHandle, vc vulnerabilityDecoratio } } +func patchCVSSMetrics(blob *v6.VulnerabilityBlob) { + for i := range blob.Severities { + sev := &blob.Severities[i] + if val, ok := sev.Value.(v6.CVSSSeverity); ok { + met, err := cvss.ParseMetricsFromVector(val.Vector) + if err != nil { + log.WithFields("vector", val.Vector, "error", err).Debug("unable to parse CVSS vector") + continue + } + newSev := CVSSSeverity{ + Vector: val.Vector, + Version: val.Version, + Metrics: CvssMetrics{ + BaseScore: met.BaseScore, + ExploitabilityScore: met.ExploitabilityScore, + ImpactScore: met.ImpactScore, + }, + } + sev.Value = newSev + } + } +} + func newOperatingSystems(oss []v6.OperatingSystem) (os []OperatingSystem) { for _, o := range oss { os = append(os, OperatingSystem{ diff --git a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go index 285addb5cf5..0536611388f 100644 --- a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go +++ b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go @@ -113,7 +113,20 @@ func TestVulnerabilities(t *testing.T) { PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), Provider: &v6.Provider{ID: "provider1"}, - BlobValue: &v6.VulnerabilityBlob{Description: "Test description"}, + BlobValue: &v6.VulnerabilityBlob{ + Description: "Test description", + Severities: []v6.Severity{ + { + Scheme: v6.SeveritySchemeCVSS, + Value: v6.CVSSSeverity{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + Version: "3.1", + }, + Source: "nvd", + Rank: 1, + }, + }, + }, }, }, nil) @@ -156,12 +169,30 @@ func TestVulnerabilities(t *testing.T) { expected := []Vulnerability{ { VulnerabilityInfo: VulnerabilityInfo{ - VulnerabilityBlob: v6.VulnerabilityBlob{Description: "Test description"}, - Provider: "provider1", - Status: "active", - PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), - ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), - WithdrawnDate: nil, + VulnerabilityBlob: v6.VulnerabilityBlob{ + Description: "Test description", + Severities: []v6.Severity{ + { + Scheme: "CVSS", + Value: CVSSSeverity{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + Version: "3.1", + Metrics: CvssMetrics{ + BaseScore: 7.5, + ExploitabilityScore: ptr(3.9), + ImpactScore: ptr(3.6), + }, + }, + Source: "nvd", + Rank: 1, + }, + }, + }, + Provider: "provider1", + Status: "active", + PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), + ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), + WithdrawnDate: nil, KnownExploited: []KnownExploited{ { CVE: "CVE-1234-5678", diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index 0883b6b81d0..937797e04c5 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/spf13/cobra" "github.com/wagoodman/go-partybus" @@ -11,7 +12,7 @@ import ( "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype" - v6 "github.com/anchore/grype/grype/db/v6" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/parsers" "github.com/anchore/grype/grype/grypeerr" @@ -35,7 +36,6 @@ import ( "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/cataloging" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" ) @@ -65,6 +65,7 @@ You can also explicitly specify the scheme to use: {{.appName}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) {{.appName}} purl:path/to/purl/file read a newline separated file of package URLs from a path on disk {{.appName}} PURL read a single package PURL directly (e.g. pkg:apk/openssl@3.2.1?distro=alpine-3.20.3) + {{.appName}} CPE read a single CPE directly (e.g. cpe:2.3:a:openssl:openssl:3.0.14:*:*:*:*:*) You can also pipe in Syft JSON directly: syft yourimage:tag -o json | {{.appName}} @@ -119,7 +120,7 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs } var vp vulnerability.Provider - var status *v6.Status + var status *vulnerability.ProviderStatus var packages []pkg.Package var s *sbom.SBOM var pkgContext pkg.Context @@ -151,11 +152,15 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs return nil }, func() (err error) { + startTime := time.Now() + defer func() { log.WithFields("time", time.Since(startTime)).Info("loaded DB") }() log.Debug("loading DB") vp, status, err = grype.LoadVulnerabilityDB(opts.ToClientConfig(), opts.ToCuratorConfig(), opts.DB.AutoUpdate) return validateDBLoad(err, status) }, func() (err error) { + startTime := time.Now() + defer func() { log.WithFields("time", time.Since(startTime)).Info("gathered packages") }() log.Debugf("gathering packages") // packages are grype.Package, not syft.Package // the SBOM is returned for downstream formatting concerns @@ -179,6 +184,7 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs return fmt.Errorf("applying vex rules: %w", err) } + startTime := time.Now() applyDistroHint(packages, &pkgContext, opts) vulnMatcher := grype.VulnerabilityMatcher{ @@ -201,25 +207,51 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs errs = appendErrors(errs, err) } - model, err := models.NewDocument(app.ID(), packages, pkgContext, *remainingMatches, ignoredMatches, vp, opts, status, models.SortByPackage) + log.WithFields("time", time.Since(startTime)).Info("found vulnerability matches") + startTime = time.Now() + + model, err := models.NewDocument(app.ID(), packages, pkgContext, *remainingMatches, ignoredMatches, vp, opts, dbInfo(status, vp), models.SortStrategy(opts.SortBy.Criteria)) if err != nil { return fmt.Errorf("failed to create document: %w", err) } if err = writer.Write(models.PresenterConfig{ - ID: app.ID(), - Document: model, - SBOM: s, - AppConfig: opts, - DBStatus: status, - Pretty: opts.Pretty, + ID: app.ID(), + Document: model, + SBOM: s, + Pretty: opts.Pretty, }); err != nil { errs = appendErrors(errs, err) } + log.WithFields("time", time.Since(startTime)).Trace("wrote vulnerability report") + return errs } +func dbInfo(status *vulnerability.ProviderStatus, vp vulnerability.Provider) any { + var providers map[string]vulnerability.DataProvenance + + if vp != nil { + providers = make(map[string]vulnerability.DataProvenance) + if dpr, ok := vp.(vulnerability.StoreMetadataProvider); ok { + dps, err := dpr.DataProvenance() + // ignore errors here + if err == nil { + providers = dps + } + } + } + + return struct { + Status *vulnerability.ProviderStatus `json:"status"` + Providers map[string]vulnerability.DataProvenance `json:"providers"` + }{ + Status: status, + Providers: providers, + } +} + func applyDistroHint(pkgs []pkg.Package, context *pkg.Context, opts *options.Grype) { if opts.Distro != "" { log.Infof("using distro: %s", opts.Distro) @@ -230,28 +262,25 @@ func applyDistroHint(pkgs []pkg.Package, context *pkg.Context, opts *options.Gry if len(split) > 1 { v = split[1] } - context.Distro = &linux.Release{ - PrettyName: d, - Name: d, - ID: d, - IDLike: []string{ - d, - }, - Version: v, - VersionID: v, + var err error + context.Distro, err = distro.NewFromNameVersion(d, v) + if err != nil { + log.WithFields("distro", opts.Distro, "error", err).Warn("unable to parse distro") } } - hasOSPackage := false + hasOSPackageWithoutDistro := false for _, p := range pkgs { switch p.Type { case syftPkg.AlpmPkg, syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.KbPkg: - hasOSPackage = true + if p.Distro == nil { + hasOSPackageWithoutDistro = true + } } } - if context.Distro == nil && hasOSPackage { - log.Warnf("Unable to determine the OS distribution. This may result in missing vulnerabilities. " + + if context.Distro == nil && hasOSPackageWithoutDistro { + log.Warnf("Unable to determine the OS distribution of some packages. This may result in missing vulnerabilities. " + "You may specify a distro using: --distro :") } } @@ -326,7 +355,7 @@ func getProviderConfig(opts *options.Grype) pkg.ProviderConfig { } } -func validateDBLoad(loadErr error, status *v6.Status) error { +func validateDBLoad(loadErr error, status *vulnerability.ProviderStatus) error { if loadErr != nil { // notify the user about grype db delete to fix checksum errors if strings.Contains(loadErr.Error(), "checksum") { @@ -340,8 +369,8 @@ func validateDBLoad(loadErr error, status *v6.Status) error { if status == nil { return fmt.Errorf("unable to determine the status of the vulnerability db") } - if status.Err != nil { - return fmt.Errorf("db could not be loaded: %w", status.Err) + if status.Error != nil { + return fmt.Errorf("db could not be loaded: %w", status.Error) } return nil } diff --git a/cmd/grype/cli/commands/root_test.go b/cmd/grype/cli/commands/root_test.go index bd1d27b80dc..e50d3408908 100644 --- a/cmd/grype/cli/commands/root_test.go +++ b/cmd/grype/cli/commands/root_test.go @@ -28,24 +28,24 @@ func Test_applyDistroHint(t *testing.T) { applyDistroHint([]pkg.Package{}, &ctx, &cfg) assert.NotNil(t, ctx.Distro) - assert.Equal(t, "alpine", ctx.Distro.Name) + assert.Equal(t, "alpine", ctx.Distro.Name()) assert.Equal(t, "3.10", ctx.Distro.Version) // does override an existing distro - cfg.Distro = "ubuntu:latest" + cfg.Distro = "ubuntu:24.04" applyDistroHint([]pkg.Package{}, &ctx, &cfg) assert.NotNil(t, ctx.Distro) - assert.Equal(t, "ubuntu", ctx.Distro.Name) - assert.Equal(t, "latest", ctx.Distro.Version) + assert.Equal(t, "ubuntu", ctx.Distro.Name()) + assert.Equal(t, "24.04", ctx.Distro.Version) // doesn't remove an existing distro when empty cfg.Distro = "" applyDistroHint([]pkg.Package{}, &ctx, &cfg) assert.NotNil(t, ctx.Distro) - assert.Equal(t, "ubuntu", ctx.Distro.Name) - assert.Equal(t, "latest", ctx.Distro.Version) + assert.Equal(t, "ubuntu", ctx.Distro.Name()) + assert.Equal(t, "24.04", ctx.Distro.Version) } func Test_getProviderConfig(t *testing.T) { diff --git a/cmd/grype/cli/options/database.go b/cmd/grype/cli/options/database.go index 73044576866..ccafe815920 100644 --- a/cmd/grype/cli/options/database.go +++ b/cmd/grype/cli/options/database.go @@ -4,6 +4,7 @@ import ( "time" "github.com/anchore/clio" + "github.com/anchore/go-homedir" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" ) @@ -25,6 +26,7 @@ type Database struct { var _ interface { clio.FieldDescriber + clio.PostLoader } = (*Database)(nil) func DefaultDatabase(id clio.Identification) Database { @@ -64,3 +66,9 @@ This file is ~156KiB as of 2024-04-17 so the download should be quick; adjust as The DB is ~156MB as of 2024-04-17 so slower connections may exceed the default timeout; adjust as needed`) descriptions.Add(&cfg.MaxUpdateCheckFrequency, `Maximum frequency to check for vulnerability database updates`) } + +func (cfg *Database) PostLoad() error { + var err error + cfg.Dir, err = homedir.Expand(cfg.Dir) + return err +} diff --git a/cmd/grype/cli/options/database_search_os.go b/cmd/grype/cli/options/database_search_os.go index 4f2f6fd2c1d..00c073dd881 100644 --- a/cmd/grype/cli/options/database_search_os.go +++ b/cmd/grype/cli/options/database_search_os.go @@ -32,9 +32,6 @@ func (o *DBSearchOSs) PostLoad() error { if err != nil { return err } - if spec != nil { - spec.AllowMultiple = true - } specs = append(specs, spec) } o.Specs = specs diff --git a/cmd/grype/cli/options/database_search_os_test.go b/cmd/grype/cli/options/database_search_os_test.go index f4ae9af4b78..26b485fc1dc 100644 --- a/cmd/grype/cli/options/database_search_os_test.go +++ b/cmd/grype/cli/options/database_search_os_test.go @@ -27,7 +27,7 @@ func TestDBSearchOSsPostLoad(t *testing.T) { OSs: []string{"ubuntu"}, }, expectedSpecs: []*v6.OSSpecifier{ - {Name: "ubuntu", AllowMultiple: true}, + {Name: "ubuntu"}, }, }, { @@ -36,7 +36,7 @@ func TestDBSearchOSsPostLoad(t *testing.T) { OSs: []string{"ubuntu@20"}, }, expectedSpecs: []*v6.OSSpecifier{ - {Name: "ubuntu", MajorVersion: "20", AllowMultiple: true}, + {Name: "ubuntu", MajorVersion: "20"}, }, }, { @@ -45,7 +45,7 @@ func TestDBSearchOSsPostLoad(t *testing.T) { OSs: []string{"ubuntu@20.04"}, }, expectedSpecs: []*v6.OSSpecifier{ - {Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", AllowMultiple: true}, + {Name: "ubuntu", MajorVersion: "20", MinorVersion: "04"}, }, }, { @@ -54,7 +54,7 @@ func TestDBSearchOSsPostLoad(t *testing.T) { OSs: []string{"ubuntu@focal"}, }, expectedSpecs: []*v6.OSSpecifier{ - {Name: "ubuntu", LabelVersion: "focal", AllowMultiple: true}, + {Name: "ubuntu", LabelVersion: "focal"}, }, }, { @@ -70,7 +70,7 @@ func TestDBSearchOSsPostLoad(t *testing.T) { OSs: []string{"ubuntu:20"}, }, expectedSpecs: []*v6.OSSpecifier{ - {Name: "ubuntu", MajorVersion: "20", AllowMultiple: true}, + {Name: "ubuntu", MajorVersion: "20"}, }, }, { diff --git a/cmd/grype/cli/options/grype.go b/cmd/grype/cli/options/grype.go index 4bd5b3f6e7d..0dab835425b 100644 --- a/cmd/grype/cli/options/grype.go +++ b/cmd/grype/cli/options/grype.go @@ -31,6 +31,7 @@ type Grype struct { Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` ShowSuppressed bool `yaml:"show-suppressed" json:"show-suppressed" mapstructure:"show-suppressed"` ByCVE bool `yaml:"by-cve" json:"by-cve" mapstructure:"by-cve"` // --by-cve, indicates if the original match vulnerability IDs should be preserved or the CVE should be used instead + SortBy SortBy `yaml:",inline" json:",inline" mapstructure:",squash"` Name string `yaml:"name" json:"name" mapstructure:"name"` DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` VexDocuments []string `yaml:"vex-documents" json:"vex-documents" mapstructure:"vex-documents"` @@ -64,6 +65,7 @@ func DefaultGrype(id clio.Identification) *Grype { CheckForAppUpdate: true, VexAdd: []string{}, MatchUpstreamKernelHeaders: false, + SortBy: defaultSortBy(), } } diff --git a/cmd/grype/cli/options/sort_by.go b/cmd/grype/cli/options/sort_by.go new file mode 100644 index 00000000000..8a7a6121880 --- /dev/null +++ b/cmd/grype/cli/options/sort_by.go @@ -0,0 +1,47 @@ +package options + +import ( + "fmt" + "strings" + + "github.com/scylladb/go-set/strset" + + "github.com/anchore/clio" + "github.com/anchore/fangs" + "github.com/anchore/grype/grype/presenter/models" +) + +var _ interface { + fangs.FlagAdder + fangs.PostLoader +} = (*SortBy)(nil) + +type SortBy struct { + Criteria string `yaml:"sort-by" json:"sort-by" mapstructure:"sort-by"` + AllowableOptions []string `yaml:"-" json:"-" mapstructure:"-"` +} + +func defaultSortBy() SortBy { + var strategies []string + for _, s := range models.SortStrategies() { + strategies = append(strategies, strings.ToLower(s.String())) + } + return SortBy{ + Criteria: models.DefaultSortStrategy.String(), + AllowableOptions: strategies, + } +} + +func (o *SortBy) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&o.Criteria, + "sort-by", "", + fmt.Sprintf("sort the match results with the given strategy, options=%v", o.AllowableOptions), + ) +} + +func (o *SortBy) PostLoad() error { + if !strset.New(o.AllowableOptions...).Has(strings.ToLower(o.Criteria)) { + return fmt.Errorf("invalid sort-by criteria: %q (allowable: %s)", o.Criteria, strings.Join(o.AllowableOptions, ", ")) + } + return nil +} diff --git a/go.mod b/go.mod index d39f72ba4f9..f6db9d072b5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/anchore/grype -go 1.24.0 +go 1.24.1 require ( github.com/CycloneDX/cyclonedx-go v0.9.2 @@ -10,27 +10,31 @@ require ( github.com/adrg/xdg v0.5.3 github.com/anchore/archiver/v3 v3.5.3-0.20241210171143-5b1d8d1c7c51 github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9 - github.com/anchore/clio v0.0.0-20241115144204-29e89f9fa837 + github.com/anchore/clio v0.0.0-20250408180537-ec8fa27f0d9f + github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537 - github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a + github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d + github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 - github.com/anchore/stereoscope v0.0.13 - github.com/anchore/syft v1.20.0 + github.com/anchore/stereoscope v0.1.4 + github.com/anchore/syft v1.25.1 github.com/aquasecurity/go-pep440-version v0.0.1 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/bmatcuk/doublestar/v2 v2.0.4 - github.com/charmbracelet/bubbletea v1.3.4 - github.com/charmbracelet/lipgloss v1.0.0 + github.com/charmbracelet/bubbletea v1.3.5 + github.com/charmbracelet/lipgloss v1.1.0 github.com/dave/jennifer v1.7.1 - github.com/docker/docker v28.0.1+incompatible + github.com/docker/docker v28.1.1+incompatible github.com/dustin/go-humanize v1.0.1 github.com/facebookincubator/nvdtools v0.1.5 - github.com/gabriel-vasile/mimetype v1.4.8 + github.com/gabriel-vasile/mimetype v1.4.9 github.com/gkampitakis/go-snaps v0.5.11 github.com/glebarez/sqlite v1.11.0 github.com/go-test/deep v1.1.1 + github.com/go-viper/mapstructure/v2 v2.2.1 + github.com/gohugoio/hashstructure v0.5.0 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.3 github.com/google/uuid v1.6.0 @@ -45,9 +49,7 @@ require ( github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08 - github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/hashstructure/v2 v2.0.2 - github.com/mitchellh/mapstructure v1.5.0 + github.com/muesli/termenv v0.16.0 github.com/olekukonko/tablewriter v0.0.5 github.com/openvex/go-vex v0.2.5 github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 @@ -55,7 +57,7 @@ require ( // pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5 github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 - github.com/spf13/afero v1.12.0 + github.com/spf13/afero v1.14.0 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 github.com/ulikunitz/xz v0.5.12 @@ -63,17 +65,13 @@ require ( github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/time v0.11.0 - golang.org/x/tools v0.31.0 + golang.org/x/tools v0.33.0 gopkg.in/yaml.v3 v3.0.1 - gorm.io/gorm v1.25.12 + gorm.io/gorm v1.26.1 ) -require github.com/DataDog/zstd v1.5.5 // indirect - -require github.com/muesli/termenv v0.16.0 - require ( cel.dev/expr v0.16.1 // indirect cloud.google.com/go v0.116.0 // indirect @@ -86,22 +84,24 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/DataDog/zstd v1.5.5 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver v1.5.0 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.11.7 // indirect - github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/ProtonMail/go-crypto v1.2.0 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect github.com/acobaugh/osrelease v0.1.0 // indirect github.com/agext/levenshtein v1.2.1 // indirect - github.com/anchore/fangs v0.0.0-20241014225144-4e1713cafd77 // indirect github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect + github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec // indirect github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect - github.com/andybalholm/brotli v1.1.1 // indirect + github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6 // indirect + github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aws/aws-sdk-go v1.44.288 // indirect @@ -110,39 +110,44 @@ require ( github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef // indirect + github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/cloudflare/circl v1.3.8 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/containerd/cgroups v1.1.0 // indirect - github.com/containerd/containerd v1.7.24 // indirect - github.com/containerd/containerd/api v1.7.19 // indirect - github.com/containerd/continuity v0.4.2 // indirect + github.com/containerd/containerd v1.7.27 // indirect + github.com/containerd/containerd/api v1.8.0 // indirect + github.com/containerd/continuity v0.4.4 // indirect github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect - github.com/containerd/ttrpc v1.2.5 // indirect + github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.1.1 // indirect - github.com/cyphar/filepath-securejoin v0.3.6 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v27.5.0+incompatible // indirect + github.com/docker/cli v28.1.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect - github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/elliotchance/phpserialize v1.4.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane v0.13.1 // indirect @@ -150,29 +155,29 @@ require ( github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/fgprof v0.9.5 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/github/go-spdx/v2 v2.3.2 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/github/go-spdx/v2 v2.3.3 // indirect github.com/gkampitakis/ciinfo v0.3.1 // indirect github.com/gkampitakis/go-diff v1.3.2 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-git/go-git/v5 v5.13.2 // indirect + github.com/go-git/go-git/v5 v5.16.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect github.com/goccy/go-yaml v1.15.13 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/licensecheck v0.3.1 // indirect - github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl/v2 v2.23.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -183,30 +188,30 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect - github.com/knqyf263/go-rpmdb v0.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/maruel/natural v1.1.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 // indirect + github.com/mholt/archives v0.1.2 // indirect + github.com/minio/minlz v1.0.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect - github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/signal v0.7.0 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect @@ -214,16 +219,18 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 // indirect github.com/nwaples/rardecode v1.1.3 // indirect + github.com/nwaples/rardecode/v2 v2.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.1.0 // indirect github.com/opencontainers/selinux v1.11.0 // indirect github.com/package-url/packageurl-go v0.1.1 // indirect github.com/pborman/indent v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/profile v1.7.0 // indirect @@ -231,25 +238,25 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect - github.com/saferwall/pe v1.5.6 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c // indirect + github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/sassoftware/go-rpmutils v0.4.0 // indirect - github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.3.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb // indirect github.com/spdx/tools-golang v0.5.5 // indirect - github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/spf13/viper v1.19.0 // indirect + github.com/spf13/viper v1.20.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/sylabs/sif/v2 v2.20.2 // indirect - github.com/sylabs/squashfs v1.0.4 // indirect + github.com/sylabs/sif/v2 v2.21.1 // indirect + github.com/sylabs/squashfs v1.0.6 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -273,29 +280,28 @@ require ( go.opentelemetry.io/otel/sdk v1.33.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.33.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/crypto v0.38.0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.40.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/api v0.215.0 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect google.golang.org/grpc v1.67.3 // indirect - google.golang.org/protobuf v1.36.3 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + google.golang.org/protobuf v1.36.4 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - modernc.org/libc v1.61.13 // indirect + modernc.org/libc v1.62.1 // indirect modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.8.2 // indirect - modernc.org/sqlite v1.35.0 // indirect + modernc.org/memory v1.9.1 // indirect + modernc.org/sqlite v1.37.0 // indirect ) // this is a breaking change, so we need to pin the version until glebarez/go-sqlite is updated to use internal/libc diff --git a/go.sum b/go.sum index d69a3797c22..2200baeef73 100644 --- a/go.sum +++ b/go.sum @@ -629,8 +629,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -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/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= @@ -648,10 +648,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -662,8 +660,10 @@ github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= -github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= +github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= @@ -684,32 +684,38 @@ github.com/anchore/archiver/v3 v3.5.3-0.20241210171143-5b1d8d1c7c51 h1:yhk+P8lF3 github.com/anchore/archiver/v3 v3.5.3-0.20241210171143-5b1d8d1c7c51/go.mod h1:nwuGSd7aZp0rtYt79YggCGafz1RYsclE7pi3fhLwvuw= github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9 h1:p0ZIe0htYOX284Y4axJaGBvXHU0VCCzLN5Wf5XbKStU= github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9/go.mod h1:3ZsFB9tzW3vl4gEiUeuSOMDnwroWxIxJelOOHUp8dSw= -github.com/anchore/clio v0.0.0-20241115144204-29e89f9fa837 h1:bIG3WsfosZsJ5LMC7PB9J/ekFM3a0j0ZEDvN3ID6GTI= -github.com/anchore/clio v0.0.0-20241115144204-29e89f9fa837/go.mod h1:tRQVKkjYeejrh9AdM0s1esbwtMU7rdHAHSQWkv4qskE= -github.com/anchore/fangs v0.0.0-20241014225144-4e1713cafd77 h1:h7+GCqazHVS5GDJYYS6wjjglYi8xFnVWMdSUukoImTM= -github.com/anchore/fangs v0.0.0-20241014225144-4e1713cafd77/go.mod h1:qbev5czQeyDO74fPNThiEKYkgt0mx1axb+5wQcxDPFY= +github.com/anchore/clio v0.0.0-20250408180537-ec8fa27f0d9f h1:jTeN+fKTXz1VFo3Zj7Msnx//s5kD6Htd+SS0z9/o7Ss= +github.com/anchore/clio v0.0.0-20250408180537-ec8fa27f0d9f/go.mod h1:jQ+jv7v9RQnc5oA+Z0rAyXsQfaCAZHwY/CJZiLVggQ4= +github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe h1:qv/xxpjF5RdKPqZjx8RM0aBi3HUCAO0DhRBMs2xhY1I= +github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe/go.mod h1:vrcYMDps9YXwwx2a9AsvipM6Fi5H9//9bymGb8G8BIQ= github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537 h1:GjNGuwK5jWjJMyVppBjYS54eOiiSNv4Ba869k4wh72Q= github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8= -github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw= -github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= +github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc= +github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50= +github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM= +github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw= github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU= github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk= +github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec h1:SjjPMOXTzpuU1ZME4XeoHyek+dry3/C7I8gzaCo02eg= +github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6 h1:Ha+LSCVuXYSYGi7wIkJK6G8g6jI3LH7y6LbyEVyp4Io= +github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6/go.mod h1:+9oM3XUy8iea/vWj9FhZ9bQGUBN8JpPxxJm5Wbcx9XM= github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8= github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ= github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE+o2gozGEBoUMpX27lsku+xrMwlmBZJtbg= github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= -github.com/anchore/stereoscope v0.0.13 h1:9Ivkh7k+vOeG3JHrt44jOg/8UdZrCvMsSjLQ7trHBig= -github.com/anchore/stereoscope v0.0.13/go.mod h1:QfhhFc2pezp5aX/dVJ5qnBFpBUv5+KUTphwaQLxMUig= -github.com/anchore/syft v1.20.0 h1:4nVM/eiqrb2GJCkW+d1xv8M5mxply8vVblpWOvVCgN8= -github.com/anchore/syft v1.20.0/go.mod h1:h8U0q+Fk7f1d9ay4oa+gDb//AJYFuQftrBLOuS6llz4= +github.com/anchore/stereoscope v0.1.4 h1:e+iT9UdUzLBabWGe84hn5sTHDRioY+4IHsVzJXuJlek= +github.com/anchore/stereoscope v0.1.4/go.mod h1:omWgXDEp/XfqCJlZXIByEo1c3ArZg/qTJ5LBKVLAIdw= +github.com/anchore/syft v1.25.1 h1:HaG5/0r1UdZ7zyscEFeFz0pQsBLTXdCgEDXa5LqFjcg= +github.com/anchore/syft v1.25.1/go.mod h1:xa15pYmHrXKe7IlvaO+EAD/krawWYUtILTpMcL/S+Gw= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ= +github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -750,10 +756,18 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef h1:TSFnfbbu2oAOuWbeDDTtwXWE6z+PmpgbSsMBeV7l0ww= github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo= +github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= +github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI= github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -772,16 +786,20 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= -github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= +github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -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/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= @@ -797,8 +815,8 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= -github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -817,12 +835,12 @@ github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8E github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA= -github.com/containerd/containerd v1.7.24/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= -github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA= -github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= -github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= -github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= +github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= +github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= +github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= +github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= +github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= @@ -833,8 +851,8 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= -github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= -github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= +github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -842,8 +860,8 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= -github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -855,12 +873,12 @@ github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4i github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= -github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= +github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= -github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= +github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -870,16 +888,14 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6Uezg github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= -github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= -github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= -github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= -github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3HMDI8hG2OY= github.com/elliotchance/phpserialize v1.4.0/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -925,21 +941,21 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/github/go-spdx/v2 v2.3.2 h1:IfdyNHTqzs4zAJjXdVQfRnxt1XMfycXoHBE2Vsm1bjs= -github.com/github/go-spdx/v2 v2.3.2/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ= +github.com/github/go-spdx/v2 v2.3.3 h1:QI7evnHWEfWkT54eJwkoV/f3a0xD3gLlnVmT5wQG6LE= +github.com/github/go-spdx/v2 v2.3.3/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ= github.com/gkampitakis/ciinfo v0.3.1 h1:lzjbemlGI4Q+XimPg64ss89x8Mf3xihJqy/0Mgagapo= github.com/gkampitakis/ciinfo v0.3.1/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.11 h1:LFG0ggUKR+KEiiaOvFCmLgJ5NO2zf93AxxddkBn3LdQ= github.com/gkampitakis/go-snaps v0.5.11/go.mod h1:PcKmy8q5Se7p48ywpogN5Td13reipz1Iivah4wrTIvY= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -955,8 +971,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 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.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= -github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= +github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -979,6 +995,8 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= @@ -989,6 +1007,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg= +github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -996,8 +1016,9 @@ github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -1079,8 +1100,8 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= -github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= @@ -1155,7 +1176,8 @@ github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= @@ -1213,8 +1235,8 @@ github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -1223,8 +1245,6 @@ github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GX github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8= github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d h1:X4cedH4Kn3JPupAwwWuo4AzYp16P0OyLO9d7OnMZc/c= github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao= -github.com/knqyf263/go-rpmdb v0.1.1 h1:oh68mTCvp1XzxdU7EfafcWzzfstUZAEa3MW0IJye584= -github.com/knqyf263/go-rpmdb v0.1.1/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -1249,8 +1269,8 @@ github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuz github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= -github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= @@ -1262,8 +1282,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= @@ -1284,13 +1304,15 @@ github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 h1:tQRHcLQwnwrPq2j2Qra/NnyjyESBGwdeBeVdAE9kXYg= -github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g= +github.com/mholt/archives v0.1.2 h1:UBSe5NfYKHI1sy+S5dJsEsG9jsKKk8NJA4HCC+xTI4A= +github.com/mholt/archives v0.1.2/go.mod h1:D7QzTHgw3ctfS6wgOO9dN+MFgdZpbksGCxprUOwZWDs= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= +github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -1299,25 +1321,23 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= -github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= @@ -1342,16 +1362,22 @@ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 h1:kpt9ZfKcm+EDG4s40hMwE//d5SBgDjUOrITReV2u4aA= +github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1/go.mod h1:qgCw4bBKZX8qMgGeEZzGFVT3notl42dBjNqO2jut0M0= +github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= +github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= +github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 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 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= @@ -1372,14 +1398,14 @@ github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrp github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -1411,8 +1437,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -1424,19 +1450,18 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c h1:8gOLsYwaY2JwlTMT4brS5/9XJdrdIbmk2obvQ748CC0= +github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c/go.mod h1:kwM/7r/rVluTE8qJbHAffduuqmSv4knVQT2IajGvSiA= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/saferwall/pe v1.5.6 h1:DrRLnoQFxHWJ5lJUmrH7X2L0xeUu6SUS95Dc61eW2Yc= -github.com/saferwall/pe v1.5.6/go.mod h1:mJx+PuptmNpoPFBNhWs/uDMFL/kTHVZIkg0d4OUJFbQ= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= +github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= @@ -1451,8 +1476,6 @@ github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= -github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d h1:RQqyEogx5J6wPdoxqL132b100j8KjcVHO1c0KLRoIhc= -github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d/go.mod h1:PegD7EVqlN88z7TpCqH92hHP+GBpfomGCCnw1PFtNOA= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -1464,22 +1487,25 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb h1:bLo8hvc8XFm9J47r690TUKBzcjSWdJDxmjXJZ+/f92U= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= @@ -1488,8 +1514,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -1513,10 +1539,10 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/sylabs/sif/v2 v2.20.2 h1:HGEPzauCHhIosw5o6xmT3jczuKEuaFzSfdjAsH33vYw= -github.com/sylabs/sif/v2 v2.20.2/go.mod h1:WyYryGRaR4Wp21SAymm5pK0p45qzZCSRiZMFvUZiuhc= -github.com/sylabs/squashfs v1.0.4 h1:uFSw7WXv7zjutPvU+JzY0nY494Vw8s4FAf4+7DhoMdI= -github.com/sylabs/squashfs v1.0.4/go.mod h1:PDgf8YmCntvN4d9Y8hBUBDCZL6qZOzOQwRGxnIdbERk= +github.com/sylabs/sif/v2 v2.21.1 h1:GZ0b5//AFAqJEChd8wHV/uSKx/l1iuGYwjR8nx+4wPI= +github.com/sylabs/sif/v2 v2.21.1/go.mod h1:YoqEGQnb5x/ItV653bawXHZJOXQaEWpGwHsSD3YePJI= +github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI= +github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE= github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= @@ -1623,14 +1649,14 @@ go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -1649,8 +1675,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1666,8 +1692,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1784,8 +1810,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1838,8 +1864,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1943,8 +1969,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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= @@ -1960,8 +1986,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1983,8 +2009,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2059,8 +2085,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2351,8 +2377,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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= @@ -2362,8 +2388,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -2378,8 +2402,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= -gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= +gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2395,21 +2419,21 @@ lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= -modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= +modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= -modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= -modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= +modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= +modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= -modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= @@ -2418,8 +2442,8 @@ modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= -modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= -modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= +modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= @@ -2428,8 +2452,8 @@ modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJ modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= -modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= +modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= @@ -2437,8 +2461,8 @@ modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= -modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw= -modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= diff --git a/grype/db/v5/distribution/curator.go b/grype/db/v5/distribution/curator.go index cff1c83d571..609d67a565b 100644 --- a/grype/db/v5/distribution/curator.go +++ b/grype/db/v5/distribution/curator.go @@ -477,6 +477,7 @@ func (c Curator) ListingFromURL() (Listing, error) { return Listing{}, fmt.Errorf("unable to create listing temp file: %w", err) } defer func() { + log.CloseAndLogError(tempFile, tempFile.Name()) err := c.fs.RemoveAll(tempFile.Name()) if err != nil { log.Errorf("failed to remove file (%s): %w", tempFile.Name(), err) diff --git a/grype/db/v5/namespace/index.go b/grype/db/v5/namespace/index.go deleted file mode 100644 index 5c207cd4f0d..00000000000 --- a/grype/db/v5/namespace/index.go +++ /dev/null @@ -1,248 +0,0 @@ -package namespace - -import ( - "fmt" - "regexp" - "sort" - "strings" - - hashiVer "github.com/anchore/go-version" - "github.com/anchore/grype/grype/db/v5/namespace/cpe" - "github.com/anchore/grype/grype/db/v5/namespace/distro" - "github.com/anchore/grype/grype/db/v5/namespace/language" - grypeDistro "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/internal" - "github.com/anchore/grype/internal/log" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -var simpleSemVer = regexp.MustCompile(`^(?P\d+)(\.(?P\d+)(\.(?P\d+(?P[^-_]+)*))?)?$`) - -type Index struct { - all []Namespace - byLanguage map[syftPkg.Language][]*language.Namespace - byDistroKey map[string][]*distro.Namespace - cpe []*cpe.Namespace -} - -func FromStrings(namespaces []string) (*Index, error) { - all := make([]Namespace, 0) - byLanguage := make(map[syftPkg.Language][]*language.Namespace) - byDistroKey := make(map[string][]*distro.Namespace) - cpeNamespaces := make([]*cpe.Namespace, 0) - - for _, n := range namespaces { - ns, err := FromString(n) - - if err != nil { - log.Warnf("unable to create namespace object from namespace=%s: %+v", n, err) - continue - } - - all = append(all, ns) - - switch nsObj := ns.(type) { - case *language.Namespace: - l := nsObj.Language() - if _, ok := byLanguage[l]; !ok { - byLanguage[l] = make([]*language.Namespace, 0) - } - - byLanguage[l] = append(byLanguage[l], nsObj) - case *distro.Namespace: - distroKey := fmt.Sprintf("%s:%s", nsObj.DistroType(), nsObj.Version()) - if _, ok := byDistroKey[distroKey]; !ok { - byDistroKey[distroKey] = make([]*distro.Namespace, 0) - } - - byDistroKey[distroKey] = append(byDistroKey[distroKey], nsObj) - case *cpe.Namespace: - cpeNamespaces = append(cpeNamespaces, nsObj) - default: - log.Warnf("unable to index namespace=%s", n) - continue - } - } - - return &Index{ - all: all, - byLanguage: byLanguage, - byDistroKey: byDistroKey, - cpe: cpeNamespaces, - }, nil -} - -func (i *Index) NamespacesForLanguage(l syftPkg.Language) []*language.Namespace { - if _, ok := i.byLanguage[l]; ok { - return i.byLanguage[l] - } - - return nil -} - -//nolint:funlen,gocognit -func (i *Index) NamespacesForDistro(d *grypeDistro.Distro) []*distro.Namespace { - if d == nil { - return nil - } - - dTy := DistroTypeString(d.Type) - - if d.IsRolling() { - distroKey := fmt.Sprintf("%s:%s", dTy, "rolling") - if v, ok := i.byDistroKey[distroKey]; ok { - return v - } - } - - var versionSegments []int - if d.Version != nil { - versionSegments = d.Version.Segments() - } - - switch d.Type { - case grypeDistro.Alpine: - if v := i.getAlpineMajorMinorNamespace(d, versionSegments); v != nil { - return v - } - - // Fall back to alpine:edge if no version segments found - // alpine:edge is labeled as alpine-x.x_alphaYYYYMMDD - distroKey := fmt.Sprintf("%s:%s", dTy, "edge") - if v, ok := i.byDistroKey[distroKey]; ok { - return v - } - case grypeDistro.Debian: - if v, ok := i.findClosestNamespace(d, versionSegments); ok { - return v - } - - if d.RawVersion == "unstable" { - distroKey := fmt.Sprintf("%s:%s", dTy, "unstable") - if v, ok := i.byDistroKey[distroKey]; ok { - return v - } - } - } - - if v, ok := i.findClosestNamespace(d, versionSegments); ok { - return v - } - - return nil -} - -func (i *Index) getAlpineMajorMinorNamespace(d *grypeDistro.Distro, versionSegments []int) []*distro.Namespace { - var hasPrerelease bool - if d.Version != nil { - hasPrerelease = d.Version.Prerelease() != "" - } - - if !hasPrerelease { - namespaces, done := i.findClosestNamespace(d, versionSegments) - if done { - return namespaces - } - } - // If the version does not match x.y.z then it is edge - // In this case it would have - or _ alpha,beta,etc - // note: later in processing we handle the alpine:edge case - return nil -} - -func (i *Index) findClosestNamespace(d *grypeDistro.Distro, versionSegments []int) ([]*distro.Namespace, bool) { - ty := DistroTypeString(d.Type) - - // look for exact match - distroKey := fmt.Sprintf("%s:%s", ty, d.FullVersion()) - if v, ok := i.byDistroKey[distroKey]; ok { - return v, true - } - - values := internal.MatchNamedCaptureGroups(simpleSemVer, d.RawVersion) - - switch { - case values["major"] == "": - // use edge - break - case values["minor"] == "": - namespaces, done := i.findHighestMatchingMajorVersionNamespaces(d, versionSegments) - if done { - return namespaces, true - } - - default: - - if len(versionSegments) >= 2 { - // try with only first two version components - distroKey = fmt.Sprintf("%s:%d.%d", ty, versionSegments[0], versionSegments[1]) - if v, ok := i.byDistroKey[distroKey]; ok { - return v, true - } - } - - if len(versionSegments) >= 1 { - // try using only major version component - distroKey = fmt.Sprintf("%s:%d", ty, versionSegments[0]) - if v, ok := i.byDistroKey[distroKey]; ok { - return v, true - } - } - } - return nil, false -} - -func (i *Index) findHighestMatchingMajorVersionNamespaces(d *grypeDistro.Distro, versionSegments []int) ([]*distro.Namespace, bool) { - // find the highest version that matches the major version - majorVersion := versionSegments[0] - - var all []*distro.Namespace - for _, vs := range i.byDistroKey { - for _, v := range vs { - if v.DistroType() == d.Type { - all = append(all, v) - } - } - } - - type namespaceVersion struct { - version *hashiVer.Version - namespace *distro.Namespace - } - - var valid []namespaceVersion - for _, v := range all { - if strings.HasPrefix(v.Version(), fmt.Sprintf("%d.", majorVersion)) { - ver, err := hashiVer.NewVersion(v.Version()) - if err != nil { - continue - } - valid = append(valid, namespaceVersion{ - version: ver, - namespace: v, - }) - } - } - - // return the highest version from valid - sort.Slice(valid, func(i, j int) bool { - return valid[i].version.GreaterThan(valid[j].version) - }) - - if len(valid) > 0 { - return []*distro.Namespace{valid[0].namespace}, true - } - return nil, false -} - -func (i *Index) CPENamespaces() []*cpe.Namespace { - return i.cpe -} - -func DistroTypeString(ty grypeDistro.Type) string { - switch ty { - case grypeDistro.CentOS, grypeDistro.RedHat, grypeDistro.Fedora, grypeDistro.RockyLinux, grypeDistro.AlmaLinux, grypeDistro.Gentoo: - return strings.ToLower(string(grypeDistro.RedHat)) - } - return strings.ToLower(string(ty)) -} diff --git a/grype/db/v5/namespace/index_test.go b/grype/db/v5/namespace/index_test.go deleted file mode 100644 index 248c02dba87..00000000000 --- a/grype/db/v5/namespace/index_test.go +++ /dev/null @@ -1,404 +0,0 @@ -package namespace - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/anchore/grype/grype/db/v5/namespace/cpe" - "github.com/anchore/grype/grype/db/v5/namespace/distro" - "github.com/anchore/grype/grype/db/v5/namespace/language" - osDistro "github.com/anchore/grype/grype/distro" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -func TestFromStringSlice(t *testing.T) { - tests := []struct { - namespaces []string - byLanguage map[syftPkg.Language][]*language.Namespace - byDistroKey map[string][]*distro.Namespace - cpe []*cpe.Namespace - }{ - { - namespaces: []string{ - "github:language:python", - "github:language:python:conda", - "debian:distro:debian:8", - "alpine:distro:alpine:3.15", - "alpine:distro:alpine:3.16", - "msrc:distro:windows:12345", - "nvd:cpe", - "github:language:ruby", - "abc.xyz:language:ruby", - "github:language:rust", - "something:language:rust", - "1234.4567:language:unknown", - "---:cpe", - "another-provider:distro:alpine:3.15", - "another-provider:distro:alpine:3.16", - }, - byLanguage: map[syftPkg.Language][]*language.Namespace{ - syftPkg.Python: { - language.NewNamespace("github", syftPkg.Python, ""), - language.NewNamespace("github", syftPkg.Python, syftPkg.Type("conda")), - }, - syftPkg.Ruby: { - language.NewNamespace("github", syftPkg.Ruby, ""), - language.NewNamespace("abc.xyz", syftPkg.Ruby, ""), - }, - syftPkg.Rust: { - language.NewNamespace("github", syftPkg.Rust, ""), - language.NewNamespace("something", syftPkg.Rust, ""), - }, - syftPkg.Language("unknown"): { - language.NewNamespace("1234.4567", syftPkg.Language("unknown"), ""), - }, - }, - byDistroKey: map[string][]*distro.Namespace{ - "debian:8": { - distro.NewNamespace("debian", osDistro.Debian, "8"), - }, - "alpine:3.15": { - distro.NewNamespace("alpine", osDistro.Alpine, "3.15"), - distro.NewNamespace("another-provider", osDistro.Alpine, "3.15"), - }, - "alpine:3.16": { - distro.NewNamespace("alpine", osDistro.Alpine, "3.16"), - distro.NewNamespace("another-provider", osDistro.Alpine, "3.16"), - }, - "windows:12345": { - distro.NewNamespace("msrc", osDistro.Windows, "12345"), - }, - }, - cpe: []*cpe.Namespace{ - cpe.NewNamespace("---"), - cpe.NewNamespace("nvd"), - }, - }, - } - - for _, test := range tests { - result, _ := FromStrings(test.namespaces) - assert.Len(t, result.all, len(test.namespaces)) - - for l, elems := range result.byLanguage { - assert.Contains(t, test.byLanguage, l) - assert.ElementsMatch(t, elems, test.byLanguage[l]) - } - - for d, elems := range result.byDistroKey { - assert.Contains(t, test.byDistroKey, d) - assert.ElementsMatch(t, elems, test.byDistroKey[d]) - } - - assert.ElementsMatch(t, result.cpe, test.cpe) - } -} - -func TestIndex_CPENamespaces(t *testing.T) { - tests := []struct { - namespaces []string - cpe []*cpe.Namespace - }{ - { - namespaces: []string{"nvd:cpe", "another-source:cpe", "x:distro:y:10"}, - cpe: []*cpe.Namespace{ - cpe.NewNamespace("nvd"), - cpe.NewNamespace("another-source"), - }, - }, - } - - for _, test := range tests { - result, _ := FromStrings(test.namespaces) - assert.Len(t, result.all, len(test.namespaces)) - assert.ElementsMatch(t, result.CPENamespaces(), test.cpe) - } -} - -func newDistro(t *testing.T, dt osDistro.Type, v string, idLikes []string) *osDistro.Distro { - d, err := osDistro.New(dt, v, idLikes...) - assert.NoError(t, err) - return d -} - -func TestIndex_NamespacesForDistro(t *testing.T) { - namespaceIndex, err := FromStrings([]string{ - "alpine:distro:alpine:2.17", - "alpine:distro:alpine:3.15", - "alpine:distro:alpine:3.16", - "alpine:distro:alpine:4.13", - "alpine:distro:alpine:edge", - "debian:distro:debian:8", - "debian:distro:debian:unstable", - "amazon:distro:amazonlinux:2", - "amazon:distro:amazonlinux:2022", - "abc.xyz:distro:unknown:123.456", - "redhat:distro:redhat:8", - "redhat:distro:redhat:9", - "other-provider:distro:debian:8", - "other-provider:distro:redhat:9", - "suse:distro:sles:12.5", - "mariner:distro:mariner:2.0", - "mariner:distro:azurelinux:3.0", - "msrc:distro:windows:471816", - "ubuntu:distro:ubuntu:18.04", - "ubuntu:distro:ubuntu:18.10", - "ubuntu:distro:ubuntu:20.04", - "ubuntu:distro:ubuntu:20.10", - "oracle:distro:oraclelinux:8", - "wolfi:distro:wolfi:rolling", - "chainguard:distro:chainguard:rolling", - "archlinux:distro:archlinux:rolling", - }) - - assert.NoError(t, err) - - tests := []struct { - name string - distro *osDistro.Distro - namespaces []*distro.Namespace - }{ - { - name: "alpine patch version matches minor version namespace", - distro: newDistro(t, osDistro.Alpine, "3.15.4", []string{"alpine"}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("alpine", osDistro.Alpine, "3.15"), - }, - }, - { - name: "alpine missing patch version matches with minor version", - distro: newDistro(t, osDistro.Alpine, "3.16", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("alpine", osDistro.Alpine, "3.16"), - }, - }, - { - name: "alpine missing minor version uses latest minor version", - distro: newDistro(t, osDistro.Alpine, "3", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("alpine", osDistro.Alpine, "3.16"), - }, - }, - { - name: "ubuntu missing minor version uses latest minor version", - distro: newDistro(t, osDistro.Ubuntu, "18", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("ubuntu", osDistro.Ubuntu, "18.10"), - }, - }, - { - name: "alpine rc version with no patch should match edge", - distro: newDistro(t, osDistro.Alpine, "3.16.4-r4", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("alpine", osDistro.Alpine, "edge"), - }, - }, - - { - name: "alpine edge version matches edge namespace", - distro: &osDistro.Distro{Type: osDistro.Alpine, Version: nil, RawVersion: "3.17.1_alpha20221002", IDLike: []string{"alpine"}}, - namespaces: []*distro.Namespace{ - distro.NewNamespace("alpine", osDistro.Alpine, "edge"), - }, - }, - { - name: "alpine raw version matches edge with - character", - distro: &osDistro.Distro{Type: osDistro.Alpine, Version: nil, RawVersion: "3.17.1-alpha20221002", IDLike: []string{"alpine"}}, - namespaces: []*distro.Namespace{ - distro.NewNamespace("alpine", osDistro.Alpine, "edge"), - }, - }, - { - name: "alpine raw version matches edge with - character no sha", - distro: newDistro(t, osDistro.Alpine, "3.17.1-alpha", []string{"alpine"}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("alpine", osDistro.Alpine, "edge"), - }, - }, - { - name: "alpine raw version matches edge with _ character no sha", - // we don't create a newDistro from this since parsing the version fails - distro: &osDistro.Distro{Type: osDistro.Alpine, Version: nil, RawVersion: "3.17.1_alpha", IDLike: []string{"alpine"}}, - namespaces: []*distro.Namespace{ - distro.NewNamespace("alpine", osDistro.Alpine, "edge"), - }, - }, - { - name: "alpine malformed version matches with closest", - distro: newDistro(t, osDistro.Alpine, "3.16.4.5", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("alpine", osDistro.Alpine, "3.16"), - }, - }, - { - name: "Debian minor version matches debian and other-provider namespaces", - distro: newDistro(t, osDistro.Debian, "8.5", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("debian", osDistro.Debian, "8"), - distro.NewNamespace("other-provider", osDistro.Debian, "8"), - }, - }, - { - name: "Redhat minor version matches redhat and other-provider namespaces", - distro: newDistro(t, osDistro.RedHat, "9.5", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("redhat", osDistro.RedHat, "9"), - distro.NewNamespace("other-provider", osDistro.RedHat, "9"), - }, - }, - { - name: "Centos minor version matches redhat and other-provider namespaces", - distro: newDistro(t, osDistro.CentOS, "9.5", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("redhat", osDistro.RedHat, "9"), - distro.NewNamespace("other-provider", osDistro.RedHat, "9"), - }, - }, - { - name: "Alma Linux minor version matches redhat and other-provider namespaces", - distro: newDistro(t, osDistro.AlmaLinux, "9.5", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("redhat", osDistro.RedHat, "9"), - distro.NewNamespace("other-provider", osDistro.RedHat, "9"), - }, - }, - { - name: "Rocky Linux minor version matches redhat and other-provider namespaces", - distro: newDistro(t, osDistro.RockyLinux, "9.5", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("redhat", osDistro.RedHat, "9"), - distro.NewNamespace("other-provider", osDistro.RedHat, "9"), - }, - }, - { - name: "SLES minor version matches suse namespace", - distro: newDistro(t, osDistro.SLES, "12.5", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("suse", osDistro.SLES, "12.5"), - }, - }, - { - name: "Windows version object matches msrc namespace with exact version", - distro: newDistro(t, osDistro.Windows, "471816", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("msrc", osDistro.Windows, "471816"), - }, - }, - { - name: "Ubuntu minor semvar matches ubuntu namespace with exact version", - distro: newDistro(t, osDistro.Ubuntu, "18.04", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("ubuntu", osDistro.Ubuntu, "18.04"), - }, - }, - { - name: "Fedora minor semvar will not match a namespace", - distro: newDistro(t, osDistro.Fedora, "31.4", []string{}), - namespaces: nil, - }, - { - name: "Amazon Linux Major semvar matches amazon namespace with exact version", - distro: newDistro(t, osDistro.AmazonLinux, "2", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("amazon", osDistro.AmazonLinux, "2"), - }, - }, - { - name: "Amazon Linux year version matches amazon namespace with exact uear", - distro: newDistro(t, osDistro.AmazonLinux, "2022", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("amazon", osDistro.AmazonLinux, "2022"), - }, - }, - { - name: "Mariner minor semvar matches no namespace", - distro: newDistro(t, osDistro.Mariner, "20.1", []string{}), - namespaces: nil, - }, - { - name: "Mariner 2.0 matches mariner namespace", - distro: newDistro(t, osDistro.Mariner, "2.0", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("mariner", "mariner", "2.0"), - }, - }, - { - name: "azurelinux 3 is matched by mariner 3 namespace", - distro: newDistro(t, osDistro.Azure, "3.0", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("mariner", osDistro.Azure, "3.0"), - }, - }, - { - name: "Oracle Linux Major semvar matches oracle namespace with exact version", - distro: newDistro(t, osDistro.OracleLinux, "8", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("oracle", osDistro.OracleLinux, "8"), - }, - }, - { - - name: "Arch Linux matches archlinux rolling namespace", - distro: newDistro(t, osDistro.ArchLinux, "", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("archlinux", osDistro.ArchLinux, "rolling"), - }, - }, - { - - name: "Wolfi matches wolfi rolling namespace", - distro: newDistro(t, osDistro.Wolfi, "20221011", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("wolfi", osDistro.Wolfi, "rolling"), - }, - }, - { - - name: "Chainguard matches chainguard rolling namespace", - distro: newDistro(t, osDistro.Chainguard, "20230214", []string{}), - namespaces: []*distro.Namespace{ - distro.NewNamespace("chainguard", osDistro.Chainguard, "rolling"), - }, - }, - { - - name: "Gentoo doesn't match any namespace since the gentoo rolling namespace doesn't exist in index", - distro: newDistro(t, osDistro.Gentoo, "", []string{}), - namespaces: nil, - }, - { - name: "Open Suse Leap semvar matches no namespace", - distro: newDistro(t, osDistro.OpenSuseLeap, "100", []string{}), - namespaces: nil, - }, - { - name: "Photon minor semvar no namespace", - distro: newDistro(t, osDistro.Photon, "20.1", []string{}), - namespaces: nil, - }, - { - name: "Busybox minor semvar matches no namespace", - distro: newDistro(t, osDistro.Busybox, "20.1", []string{}), - namespaces: nil, - }, - { - name: "debian unstable", - distro: &osDistro.Distro{ - Type: osDistro.Debian, - RawVersion: "unstable", - Version: nil, - }, - namespaces: []*distro.Namespace{ - distro.NewNamespace("debian", osDistro.Debian, "unstable"), - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - namespaces := namespaceIndex.NamespacesForDistro(test.distro) - assert.ElementsMatch(t, test.namespaces, namespaces) - }) - } -} diff --git a/grype/db/v5/pkg/qualifier/from_json.go b/grype/db/v5/pkg/qualifier/from_json.go index a06e76dc64f..dbb661ee4af 100644 --- a/grype/db/v5/pkg/qualifier/from_json.go +++ b/grype/db/v5/pkg/qualifier/from_json.go @@ -3,7 +3,7 @@ package qualifier import ( "encoding/json" - "github.com/mitchellh/mapstructure" + "github.com/go-viper/mapstructure/v2" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/platformcpe" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity" diff --git a/grype/db/v6/affected_package_store.go b/grype/db/v6/affected_package_store.go index 8701c250f3e..59ef9da44b5 100644 --- a/grype/db/v6/affected_package_store.go +++ b/grype/db/v6/affected_package_store.go @@ -4,10 +4,10 @@ import ( "errors" "fmt" "regexp" - "sort" "strings" "time" + "golang.org/x/exp/maps" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -25,7 +25,6 @@ var AnyOSSpecified *OSSpecifier var AnyPackageSpecified *PackageSpecifier var ErrMissingOSIdentification = errors.New("missing OS name or codename") var ErrOSNotPresent = errors.New("OS not present") -var ErrMultipleOSMatches = errors.New("multiple OS matches found but not allowed") var ErrLimitReached = errors.New("query limit reached") type GetAffectedPackageOptions struct { @@ -98,11 +97,11 @@ type OSSpecifier struct { // MinorVersion is the second field in the VERSION_ID field in /etc/os-release (e.g. 0 in "7.0.1406") MinorVersion string + // RemainingVersion is anything after the minor version in the VERSION_ID field in /etc/os-release (e.g. 1406 in "7.0.1406") + RemainingVersion string + // LabelVersion is a string that represents a floating version (e.g. "edge" or "unstable") or is the CODENAME field in /etc/os-release (e.g. "wheezy" for debian 7) LabelVersion string - - // AllowMultiple specifies whether we intend to allow for multiple distro identities to be matched. - AllowMultiple bool } func (d *OSSpecifier) String() string { @@ -116,10 +115,7 @@ func (d *OSSpecifier) String() string { var version string if d.MajorVersion != "" { - version = d.MajorVersion - if d.MinorVersion != "" { - version += "." + d.MinorVersion - } + version = d.version() } else { version = d.LabelVersion } @@ -136,19 +132,17 @@ func (d *OSSpecifier) String() string { } func (d OSSpecifier) version() string { - if d.MajorVersion != "" && d.MinorVersion != "" { - return d.MajorVersion + "." + d.MinorVersion - } - if d.MajorVersion != "" { + if d.MinorVersion != "" { + if d.RemainingVersion != "" { + return d.MajorVersion + "." + d.MinorVersion + "." + d.RemainingVersion + } + return d.MajorVersion + "." + d.MinorVersion + } return d.MajorVersion } - if d.LabelVersion != "" { - return d.LabelVersion - } - - return "" + return d.LabelVersion } func (d OSSpecifiers) String() string { @@ -509,7 +503,7 @@ func (s *affectedPackageStore) handleVulnerabilityOptions(query *gorm.DB, config } func (s *affectedPackageStore) handleOSOptions(query *gorm.DB, configs []*OSSpecifier) (*gorm.DB, error) { - resolvedDistroMap := make(map[int64]OperatingSystem) + ids := map[int64]struct{}{} if len(configs) == 0 { configs = append(configs, AnyOSSpecified) @@ -524,15 +518,12 @@ func (s *affectedPackageStore) handleOSOptions(query *gorm.DB, configs []*OSSpec return nil, fmt.Errorf("unable to resolve distro: %w", err) } - switch { - case len(curResolvedDistros) == 0: + if len(curResolvedDistros) == 0 { return nil, ErrOSNotPresent - case len(curResolvedDistros) > 1 && !config.AllowMultiple: - return nil, ErrMultipleOSMatches } hasSpecific = true for _, d := range curResolvedDistros { - resolvedDistroMap[int64(d.ID)] = d + ids[int64(d.ID)] = struct{}{} } case config == AnyOSSpecified: // TODO: one enhancement we may want to do later is "has OS defined but is not specific" which this does NOT cover. This is "may or may not have an OS defined" which is different. @@ -546,30 +537,14 @@ func (s *affectedPackageStore) handleOSOptions(query *gorm.DB, configs []*OSSpec return nil, fmt.Errorf("cannot mix specific distro with any or none distro specifiers") } - var resolvedDistros []OperatingSystem switch { case hasAny: return query, nil case hasNone: return query.Where("operating_system_id IS NULL"), nil - case hasSpecific: - for _, d := range resolvedDistroMap { - resolvedDistros = append(resolvedDistros, d) - } - sort.Slice(resolvedDistros, func(i, j int) bool { - return resolvedDistros[i].ID < resolvedDistros[j].ID - }) } - query = query.Joins("JOIN operating_systems ON affected_package_handles.operating_system_id = operating_systems.id") - - if len(resolvedDistros) > 0 { - ids := make([]ID, len(resolvedDistros)) - for i, d := range resolvedDistros { - ids[i] = d.ID - } - query = query.Where("operating_systems.id IN ?", ids) - } + query = query.Where("affected_package_handles.operating_system_id IN ?", maps.Keys(ids)) return query, nil } diff --git a/grype/db/v6/affected_package_store_test.go b/grype/db/v6/affected_package_store_test.go index 11ebb09e65e..3f7ede29f4e 100644 --- a/grype/db/v6/affected_package_store_test.go +++ b/grype/db/v6/affected_package_store_test.go @@ -817,29 +817,16 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { expected: []AffectedPackageHandle{*pkg2d1}, }, { - name: "distro major version only (allow multiple)", + name: "distro major version only", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ OSs: []*OSSpecifier{{ - Name: "ubuntu", - MajorVersion: "20", - AllowMultiple: true, + Name: "ubuntu", + MajorVersion: "20", }}, }, expected: []AffectedPackageHandle{*pkg2d1, *pkg2d2}, }, - { - name: "distro major version only (default)", - pkg: pkgFromName(pkg2d1.Package.Name), - options: &GetAffectedPackageOptions{ - OSs: []*OSSpecifier{{ - Name: "ubuntu", - MajorVersion: "20", - AllowMultiple: false, - }}, - }, - wantErr: expectErrIs(t, ErrMultipleOSMatches), - }, { name: "distro codename", pkg: pkgFromName(pkg2d1.Package.Name), @@ -1005,7 +992,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { debian10 := &OperatingSystem{Name: "debian", ReleaseID: "debian", MajorVersion: "10"} alpine318 := &OperatingSystem{Name: "alpine", ReleaseID: "alpine", MajorVersion: "3", MinorVersion: "18"} alpineEdge := &OperatingSystem{Name: "alpine", ReleaseID: "alpine", LabelVersion: "edge"} - debianTrixie := &OperatingSystem{Name: "debian", ReleaseID: "debian", LabelVersion: "trixie"} + debianUnstable := &OperatingSystem{Name: "debian", ReleaseID: "debian", LabelVersion: "unstable"} debian7 := &OperatingSystem{Name: "debian", ReleaseID: "debian", MajorVersion: "7", LabelVersion: "wheezy"} wolfi := &OperatingSystem{Name: "wolfi", ReleaseID: "wolfi", MajorVersion: "20230201"} arch := &OperatingSystem{Name: "arch", ReleaseID: "arch", MajorVersion: "20241110", MinorVersion: "0"} @@ -1023,7 +1010,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { debian10, alpine318, alpineEdge, - debianTrixie, + debianUnstable, debian7, wolfi, arch, @@ -1143,7 +1130,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { MajorVersion: "13", LabelVersion: "trixie", }, - expected: []OperatingSystem{*debianTrixie}, + expected: []OperatingSystem{*debianUnstable}, }, { name: "debian by codename", diff --git a/grype/db/v6/data.go b/grype/db/v6/data.go index 42117c0d920..df4fbe34e2b 100644 --- a/grype/db/v6/data.go +++ b/grype/db/v6/data.go @@ -42,7 +42,7 @@ func KnownOperatingSystemSpecifierOverrides() []OperatingSystemSpecifierOverride // // depending where the team is during the development cycle you will see different behavior, making automating // this a little challenging. - {Alias: "debian", Codename: "trixie", Rolling: true}, // is currently sid, which is considered rolling + {Alias: "debian", Codename: "trixie", Rolling: true, ReplacementLabelVersion: strRef("unstable")}, // is currently sid, which is considered rolling } } @@ -72,6 +72,9 @@ func KnownPackageSpecifierOverrides() []PackageSpecifierOverride { // jenkins plugins are a special case since they are always considered to be within the java ecosystem {Ecosystem: string(pkg.JenkinsPluginPkg), ReplacementEcosystem: ptr(string(pkg.JavaPkg))}, + + // legacy cases + {Ecosystem: "pecl", ReplacementEcosystem: ptr(string(pkg.PhpPeclPkg))}, } // remap package URL types to syft package types diff --git a/grype/db/v6/db.go b/grype/db/v6/db.go index 84630538302..43dba7a33f0 100644 --- a/grype/db/v6/db.go +++ b/grype/db/v6/db.go @@ -9,6 +9,7 @@ import ( "gorm.io/gorm" "github.com/anchore/grype/grype/db/internal/gormadapter" + "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) @@ -72,7 +73,7 @@ type Writer interface { type Curator interface { Reader() (Reader, error) - Status() Status + Status() vulnerability.ProviderStatus Delete() error Update() (bool, error) Import(dbArchivePath string) error @@ -101,7 +102,9 @@ func Hydrater() func(string) error { // we don't pass any data initialization here because the data is already in the db archive and we do not want // to affect the entries themselves, only indexes and schema. s, err := newStore(Config{DBDirPath: path}, false, true) - log.CloseAndLogError(s, path) + if s != nil { + log.CloseAndLogError(s, path) + } return err } } diff --git a/grype/db/v6/db_metadata_store_test.go b/grype/db/v6/db_metadata_store_test.go index 8eb6ef12b21..048f007b26b 100644 --- a/grype/db/v6/db_metadata_store_test.go +++ b/grype/db/v6/db_metadata_store_test.go @@ -19,6 +19,17 @@ func TestDbMetadataStore_empty(t *testing.T) { require.NotNil(t, actualMetadata) } +func TestDbMetadataStore_oldDb(t *testing.T) { + db := setupTestStore(t).db + require.NoError(t, db.Where("true").Model(DBMetadata{}).Update("Model", "5").Error) // old database version + s := newDBMetadataStore(db) + + // attempt to fetch a non-existent record + actualMetadata, err := s.GetDBMetadata() + require.NoError(t, err) + require.NotNil(t, actualMetadata) +} + func TestDbMetadataStore(t *testing.T) { s := newDBMetadataStore(setupTestStore(t).db) diff --git a/grype/db/v6/distribution/client.go b/grype/db/v6/distribution/client.go index 171f575880e..92ec2e4afb7 100644 --- a/grype/db/v6/distribution/client.go +++ b/grype/db/v6/distribution/client.go @@ -40,7 +40,8 @@ type Config struct { type Client interface { Latest() (*LatestDocument, error) IsUpdateAvailable(current *v6.Description) (*Archive, error) - Download(archive Archive, dest string, downloadProgress *progress.Manual) (string, error) + ResolveArchiveURL(archive Archive) (string, error) + Download(url, dest string, downloadProgress *progress.Manual) (string, error) } type client struct { @@ -117,8 +118,8 @@ func (c client) isUpdateAvailable(current *v6.Description, candidate *LatestDocu } // compare created data to current db date - if isSupersededBy(current, candidate.Archive.Description) { - log.Debugf("database update available: %s", candidate.Archive.Description) + if isSupersededBy(current, candidate.Description) { + log.Debugf("database update available: %s", candidate.Description) return &candidate.Archive, message } @@ -126,23 +127,10 @@ func (c client) isUpdateAvailable(current *v6.Description, candidate *LatestDocu return nil, message } -func (c client) Download(archive Archive, dest string, downloadProgress *progress.Manual) (string, error) { - defer downloadProgress.SetCompleted() - - if err := os.MkdirAll(dest, 0700); err != nil { - return "", fmt.Errorf("unable to create db download root dir: %w", err) - } - - // note: as much as I'd like to use the afero FS abstraction here, the go-getter library does not support it - tempDir, err := os.MkdirTemp(dest, "grype-db-download") - if err != nil { - return "", fmt.Errorf("unable to create db client temp dir: %w", err) - } - +func (c client) ResolveArchiveURL(archive Archive) (string, error) { // download the db to the temp dir u, err := url.Parse(c.latestURL()) if err != nil { - removeAllOrLog(afero.NewOsFs(), tempDir) return "", fmt.Errorf("unable to parse db URL %q: %w", c.latestURL(), err) } @@ -156,8 +144,24 @@ func (c client) Download(archive Archive, dest string, downloadProgress *progres } u.RawQuery = query.Encode() + return u.String(), nil +} + +func (c client) Download(archiveURL, dest string, downloadProgress *progress.Manual) (string, error) { + defer downloadProgress.SetCompleted() + + if err := os.MkdirAll(dest, 0700); err != nil { + return "", fmt.Errorf("unable to create db download root dir: %w", err) + } + + // note: as much as I'd like to use the afero FS abstraction here, the go-getter library does not support it + tempDir, err := os.MkdirTemp(dest, "grype-db-download") + if err != nil { + return "", fmt.Errorf("unable to create db client temp dir: %w", err) + } + // go-getter will automatically extract all files within the archive to the temp dir - err = c.dbDownloader.GetToDir(tempDir, u.String(), downloadProgress) + err = c.dbDownloader.GetToDir(tempDir, archiveURL, downloadProgress) if err != nil { removeAllOrLog(afero.NewOsFs(), tempDir) return "", fmt.Errorf("unable to download db: %w", err) @@ -173,6 +177,7 @@ func (c client) Latest() (*LatestDocument, error) { return nil, fmt.Errorf("unable to create listing temp file: %w", err) } defer func() { + log.CloseAndLogError(tempFile, tempFile.Name()) err := c.fs.RemoveAll(tempFile.Name()) if err != nil { log.WithFields("error", err, "file", tempFile.Name()).Errorf("failed to remove file") diff --git a/grype/db/v6/distribution/client_test.go b/grype/db/v6/distribution/client_test.go index 183302e0d96..f8f72bbf1b3 100644 --- a/grype/db/v6/distribution/client_test.go +++ b/grype/db/v6/distribution/client_test.go @@ -131,10 +131,6 @@ func TestClient_Latest(t *testing.T) { func TestClient_Download(t *testing.T) { destDir := t.TempDir() - archive := &Archive{ - Path: "path/to/archive.tar.gz", - Checksum: "checksum123", - } setup := func() (Client, *mockGetter) { mg := new(mockGetter) @@ -152,9 +148,10 @@ func TestClient_Download(t *testing.T) { t.Run("successful download", func(t *testing.T) { c, mg := setup() - mg.On("GetToDir", mock.Anything, "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123", mock.Anything).Return(nil) + url := "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123" + mg.On("GetToDir", mock.Anything, url, mock.Anything).Return(nil) - tempDir, err := c.Download(*archive, destDir, &progress.Manual{}) + tempDir, err := c.Download(url, destDir, &progress.Manual{}) require.NoError(t, err) require.True(t, len(tempDir) > 0) @@ -163,9 +160,10 @@ func TestClient_Download(t *testing.T) { t.Run("download error", func(t *testing.T) { c, mg := setup() - mg.On("GetToDir", mock.Anything, "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123", mock.Anything).Return(errors.New("download failed")) + url := "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123" + mg.On("GetToDir", mock.Anything, url, mock.Anything).Return(errors.New("download failed")) - tempDir, err := c.Download(*archive, destDir, &progress.Manual{}) + tempDir, err := c.Download(url, destDir, &progress.Manual{}) require.Error(t, err) require.Empty(t, tempDir) require.Contains(t, err.Error(), "unable to download db") @@ -175,10 +173,11 @@ func TestClient_Download(t *testing.T) { t.Run("nested into dir that does not exist", func(t *testing.T) { c, mg := setup() - mg.On("GetToDir", mock.Anything, "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123", mock.Anything).Return(nil) + url := "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123" + mg.On("GetToDir", mock.Anything, url, mock.Anything).Return(nil) nestedPath := filepath.Join(destDir, "nested") - tempDir, err := c.Download(*archive, nestedPath, &progress.Manual{}) + tempDir, err := c.Download(url, nestedPath, &progress.Manual{}) require.NoError(t, err) require.True(t, len(tempDir) > 0) diff --git a/grype/db/v6/distribution/latest.go b/grype/db/v6/distribution/latest.go index 176ad3e40bd..fb9e5a1a646 100644 --- a/grype/db/v6/distribution/latest.go +++ b/grype/db/v6/distribution/latest.go @@ -52,7 +52,7 @@ func NewLatestDocument(entries ...Archive) *LatestDocument { // sort from most recent to the least recent sort.SliceStable(validEntries, func(i, j int) bool { - return validEntries[i].Description.Built.After(entries[j].Description.Built.Time) + return validEntries[i].Built.After(entries[j].Built.Time) }) return &LatestDocument{ @@ -109,15 +109,15 @@ func (l LatestDocument) Write(writer io.Writer) error { l.Status = LifecycleStatus } - if l.Archive.Path == "" { + if l.Path == "" { return fmt.Errorf("missing archive path") } - if l.Archive.Checksum == "" { + if l.Checksum == "" { return fmt.Errorf("missing archive checksum") } - if l.Archive.Description.Built.Time.IsZero() { + if l.Built.IsZero() { return fmt.Errorf("missing built time") } diff --git a/grype/db/v6/import_metadata.go b/grype/db/v6/import_metadata.go index 1456615a95e..b18e8fa22c0 100644 --- a/grype/db/v6/import_metadata.go +++ b/grype/db/v6/import_metadata.go @@ -18,8 +18,8 @@ import ( const ImportMetadataFileName = "import.json" type ImportMetadata struct { - Digest string `json:"digest"` - + Digest string `json:"digest"` + Source string `json:"source,omitempty"` ClientVersion string `json:"client_version"` } @@ -59,7 +59,7 @@ func CalculateDBDigest(fs afero.Fs, dbFilePath string) (string, error) { return fmt.Sprintf("xxh64:%s", digest), nil } -func WriteImportMetadata(fs afero.Fs, dbDir string) (*ImportMetadata, error) { +func WriteImportMetadata(fs afero.Fs, dbDir, source string) (*ImportMetadata, error) { metadataFilePath := filepath.Join(dbDir, ImportMetadataFileName) f, err := fs.OpenFile(metadataFilePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { @@ -72,10 +72,10 @@ func WriteImportMetadata(fs afero.Fs, dbDir string) (*ImportMetadata, error) { return nil, fmt.Errorf("failed to calculate checksum for DB file: %w", err) } - return writeImportMetadata(f, checksums) + return writeImportMetadata(f, checksums, source) } -func writeImportMetadata(writer io.Writer, checksums string) (*ImportMetadata, error) { +func writeImportMetadata(writer io.Writer, checksums, source string) (*ImportMetadata, error) { if checksums == "" { return nil, fmt.Errorf("checksum is required") } @@ -89,6 +89,7 @@ func writeImportMetadata(writer io.Writer, checksums string) (*ImportMetadata, e doc := ImportMetadata{ Digest: checksums, + Source: source, ClientVersion: schemaver.New(ModelVersion, Revision, Addition).String(), } diff --git a/grype/db/v6/import_metadata_test.go b/grype/db/v6/import_metadata_test.go index d4bc8a58d48..5b1dd13a98e 100644 --- a/grype/db/v6/import_metadata_test.go +++ b/grype/db/v6/import_metadata_test.go @@ -44,9 +44,10 @@ func TestReadImportMetadata(t *testing.T) { }, { name: "valid metadata", - fileContent: `{"digest": "xxh64:testdigest", "client_version": "1.0.0"}`, + fileContent: `{"digest": "xxh64:testdigest", "source": "http://localhost:1234/archive.tar.gz", "client_version": "1.0.0"}`, expectedResult: &ImportMetadata{ Digest: "xxh64:testdigest", + Source: "http://localhost:1234/archive.tar.gz", ClientVersion: "1.0.0", }, }, @@ -106,7 +107,8 @@ func TestWriteImportMetadata(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer - claim, err := writeImportMetadata(&buf, tc.checksum) + src := "source!" + claim, err := writeImportMetadata(&buf, tc.checksum, src) tc.wantErr(t, err) if err == nil { @@ -120,6 +122,7 @@ func TestWriteImportMetadata(t *testing.T) { assert.Equal(t, tc.checksum, claim.Digest) assert.Equal(t, tc.expectedVersion, doc.ClientVersion) assert.Equal(t, tc.expectedVersion, claim.ClientVersion) + assert.Equal(t, src, doc.Source) } }) } diff --git a/grype/db/v6/installation/curator.go b/grype/db/v6/installation/curator.go index 5050c2f3fe2..cb408c5fbff 100644 --- a/grype/db/v6/installation/curator.go +++ b/grype/db/v6/installation/curator.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strconv" "strings" "time" @@ -20,6 +21,7 @@ import ( db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/log" @@ -116,10 +118,19 @@ func (c curator) Reader() (db.Reader, error) { mon.Set("rehydrating DB") log.Info("rehydrating DB") + // we're not changing the source of the DB, so we just want to use any existing value. + // if the source is empty/does not exist, it will be empty in the new metadata. + var source string + im, err := db.ReadImportMetadata(c.fs, c.config.DBDirectoryPath()) + if err == nil && im != nil { + // ignore errors, as this is just a best-effort to get the source + source = im.Source + } + // this is a condition where an old client imported a DB with additional capabilities than it can handle at hydration. // this could lead to missing indexes and degraded performance now that a newer client is running (that can handle these capabilities). // the only sensible thing to do is to rehydrate the existing DB to ensure indexes are up-to-date with the current client's capabilities. - if err := c.hydrate(c.config.DBDirectoryPath(), mon); err != nil { + if err := c.hydrate(c.config.DBDirectoryPath(), source, mon); err != nil { log.WithFields("error", err).Warn("unable to rehydrate DB") } mon.Set("rehydrated") @@ -139,38 +150,45 @@ func (c curator) Reader() (db.Reader, error) { return s, nil } -func (c curator) Status() db.Status { +func (c curator) Status() vulnerability.ProviderStatus { dbFile := c.config.DBFilePath() - d, err := db.ReadDescription(dbFile) - if err != nil { - return db.Status{ - Path: dbFile, - Err: err, + d, validateErr := db.ReadDescription(dbFile) + if validateErr != nil { + return vulnerability.ProviderStatus{ + Path: dbFile, + Error: validateErr, } } if d == nil { - return db.Status{ - Path: dbFile, - Err: fmt.Errorf("database not found at %q", dbFile), + return vulnerability.ProviderStatus{ + Path: dbFile, + Error: fmt.Errorf("database not found at %q", dbFile), } } - err = c.validateAge(d) - digest, checksumErr := c.validateIntegrity(d) + validateErr = c.validateAge(d) + _, checksumErr := c.validateIntegrity(d) if checksumErr != nil && c.config.ValidateChecksum { - if err != nil { - err = errors.Join(err, checksumErr) + if validateErr != nil { + validateErr = errors.Join(validateErr, checksumErr) } else { - err = checksumErr + validateErr = checksumErr } } - return db.Status{ - Built: db.Time{Time: d.Built.Time}, + var source string + im, readErr := db.ReadImportMetadata(c.fs, c.config.DBDirectoryPath()) + if readErr == nil && im != nil { + // only make a best-effort to get the source + source = im.Source + } + + return vulnerability.ProviderStatus{ + Built: d.Built.Time, SchemaVersion: d.SchemaVersion.String(), + From: source, Path: dbFile, - Checksum: digest, - Err: err, + Error: validateErr, } } @@ -254,6 +272,7 @@ func (c curator) isUpdateCheckAllowed() bool { func (c curator) update(current *db.Description) (*distribution.Archive, error) { mon := newMonitor() defer mon.SetCompleted() + startTime := time.Now() mon.Set("checking for update") update, checkErr := c.client.IsUpdateAvailable(current) @@ -273,14 +292,21 @@ func (c curator) update(current *db.Description) (*distribution.Archive, error) return nil, checkErr } - log.Infof("downloading new vulnerability DB") + log.Info("downloading new vulnerability DB") mon.Set("downloading") - dest, err := c.client.Download(*update, filepath.Dir(c.config.DBRootDir), mon.downloadProgress.Manual) + url, err := c.client.ResolveArchiveURL(*update) + if err != nil { + return nil, fmt.Errorf("unable to resolve vulnerability DB URL: %w", err) + } + dest, err := c.client.Download(url, filepath.Dir(c.config.DBRootDir), mon.downloadProgress.Manual) if err != nil { return nil, fmt.Errorf("unable to update vulnerability database: %w", err) } + + log.WithFields("url", url, "time", time.Since(startTime)).Info("downloaded vulnerability DB") + mon.downloadProgress.SetCompleted() - if err = c.activate(dest, mon); err != nil { + if err = c.activate(dest, url, mon); err != nil { return nil, fmt.Errorf("unable to activate new vulnerability database: %w", err) } @@ -382,41 +408,57 @@ func (c curator) setLastSuccessfulUpdateCheck() { _, _ = fmt.Fprintf(fh, "%s", time.Now().UTC().Format(time.RFC3339)) } -// Import takes a DB archive file and imports it into the final DB location. -func (c curator) Import(path string) error { +// Import takes a DB file path, archive file path, or URL and imports it into the final DB location. +func (c curator) Import(reference string) error { mon := newMonitor() - mon.Set("unarchiving") + mon.Set("preparing") defer mon.SetCompleted() if err := os.MkdirAll(c.config.DBRootDir, 0700); err != nil { return fmt.Errorf("unable to create db root dir: %w", err) } - // note: the temp directory is persisted upon download/validation/activation failure to allow for investigation - tempDir, err := os.MkdirTemp(c.config.DBRootDir, fmt.Sprintf("tmp-v%v-import", db.ModelVersion)) - if err != nil { - return fmt.Errorf("unable to create db import temp dir: %w", err) - } + var tempDir, url string + if isURL(reference) { + log.Info("downloading new vulnerability DB") + mon.Set("downloading") + var err error - if strings.HasSuffix(path, ".db") { - // this is a raw DB file, copy it to the temp dir - log.Trace("copying DB") - if err := file.CopyFile(afero.NewOsFs(), path, filepath.Join(tempDir, db.VulnerabilityDBFileName)); err != nil { - return fmt.Errorf("unable to copy DB file: %w", err) + tempDir, err = c.client.Download(reference, filepath.Dir(c.config.DBRootDir), mon.downloadProgress.Manual) + if err != nil { + return fmt.Errorf("unable to update vulnerability database: %w", err) } + + url = reference } else { - // assume it is an archive - log.Trace("unarchiving DB") - err = archiver.Unarchive(path, tempDir) + // note: the temp directory is persisted upon download/validation/activation failure to allow for investigation + var err error + tempDir, err = os.MkdirTemp(c.config.DBRootDir, fmt.Sprintf("tmp-v%v-import", db.ModelVersion)) if err != nil { - return err + return fmt.Errorf("unable to create db import temp dir: %w", err) + } + + url = "manual import" + + if strings.HasSuffix(reference, ".db") { + // this is a raw DB file, copy it to the temp dir + log.Trace("copying DB") + if err := file.CopyFile(afero.NewOsFs(), reference, filepath.Join(tempDir, db.VulnerabilityDBFileName)); err != nil { + return fmt.Errorf("unable to copy DB file: %w", err) + } + } else { + // assume it is an archive + log.Info("unarchiving DB") + err := archiver.Unarchive(reference, tempDir) + if err != nil { + return err + } } } mon.downloadProgress.SetCompleted() - err = c.activate(tempDir, mon) - if err != nil { + if err := c.activate(tempDir, url, mon); err != nil { removeAllOrLog(c.fs, tempDir) return err } @@ -426,20 +468,31 @@ func (c curator) Import(path string) error { return nil } +var urlPrefixPattern = regexp.MustCompile("^[a-zA-Z]+://") + +func isURL(reference string) bool { + return urlPrefixPattern.MatchString(reference) +} + // activate swaps over the downloaded db to the application directory, calculates the checksum, and records the checksums to a file. -func (c curator) activate(dbDirPath string, mon monitor) error { +func (c curator) activate(dbDirPath, url string, mon monitor) error { defer mon.SetCompleted() - if err := c.hydrate(dbDirPath, mon); err != nil { + startTime := time.Now() + if err := c.hydrate(dbDirPath, url, mon); err != nil { return fmt.Errorf("failed to hydrate database: %w", err) } + log.WithFields("time", time.Since(startTime)).Trace("hydrated db") + startTime = time.Now() + defer func() { log.WithFields("time", time.Since(startTime)).Trace("replaced db") }() + mon.Set("activating") return c.replaceDB(dbDirPath) } -func (c curator) hydrate(dbDirPath string, mon monitor) error { +func (c curator) hydrate(dbDirPath, from string, mon monitor) error { if c.hydrator != nil { mon.Set("hydrating") if err := c.hydrator(dbDirPath); err != nil { @@ -450,7 +503,7 @@ func (c curator) hydrate(dbDirPath string, mon monitor) error { mon.Set("hashing") - doc, err := db.WriteImportMetadata(c.fs, dbDirPath) + doc, err := db.WriteImportMetadata(c.fs, dbDirPath, from) if err != nil { return fmt.Errorf("failed to write checksums file: %w", err) } diff --git a/grype/db/v6/installation/curator_test.go b/grype/db/v6/installation/curator_test.go index e207575d9da..c00a893fbfe 100644 --- a/grype/db/v6/installation/curator_test.go +++ b/grype/db/v6/installation/curator_test.go @@ -36,8 +36,12 @@ func (m *mockClient) IsUpdateAvailable(current *db.Description) (*distribution.A return args.Get(0).(*distribution.Archive), nil } -func (m *mockClient) Download(archive distribution.Archive, dest string, downloadProgress *progress.Manual) (string, error) { - args := m.Called(archive, dest, downloadProgress) +func (m *mockClient) ResolveArchiveURL(_ distribution.Archive) (string, error) { + return "http://localhost/archive.tar.zst", nil +} + +func (m *mockClient) Download(url, dest string, downloadProgress *progress.Manual) (string, error) { + args := m.Called(url, dest, downloadProgress) return args.String(0), args.Error(1) } @@ -175,7 +179,7 @@ func writeTestDB(t *testing.T, fs afero.Fs, dir string) string { require.NoError(t, rw.SetDBMetadata()) require.NoError(t, rw.Close()) - doc, err := db.WriteImportMetadata(fs, dir) + doc, err := db.WriteImportMetadata(fs, dir, "source") require.NoError(t, err) require.NotNil(t, doc) diff --git a/grype/db/v6/severity.go b/grype/db/v6/severity.go index 0c7c4b36979..74b14fefe18 100644 --- a/grype/db/v6/severity.go +++ b/grype/db/v6/severity.go @@ -2,15 +2,9 @@ package v6 import ( "fmt" - "math" - "strings" - - gocvss20 "github.com/pandatix/go-cvss/20" - gocvss30 "github.com/pandatix/go-cvss/30" - gocvss31 "github.com/pandatix/go-cvss/31" - gocvss40 "github.com/pandatix/go-cvss/40" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/cvss" "github.com/anchore/grype/internal/log" ) @@ -35,7 +29,7 @@ func extractSeverity(severity any) (vulnerability.Severity, error) { case string: return vulnerability.ParseSeverity(sev), nil case CVSSSeverity: - metrics, err := parseCVSS(sev.Vector) + metrics, err := cvss.ParseMetricsFromVector(sev.Vector) if err != nil { return vulnerability.UnknownSeverity, fmt.Errorf("unable to parse CVSS vector: %w", err) } @@ -48,67 +42,6 @@ func extractSeverity(severity any) (vulnerability.Severity, error) { } } -func parseCVSS(vector string) (*vulnerability.CvssMetrics, error) { - switch { - case strings.HasPrefix(vector, "CVSS:3.0"): - cvss, err := gocvss30.ParseVector(vector) - if err != nil { - return nil, fmt.Errorf("unable to parse CVSS v3 vector: %w", err) - } - ex := roundScore(cvss.Exploitability()) - im := roundScore(cvss.Impact()) - return &vulnerability.CvssMetrics{ - BaseScore: roundScore(cvss.BaseScore()), - ExploitabilityScore: &ex, - ImpactScore: &im, - }, nil - case strings.HasPrefix(vector, "CVSS:3.1"): - cvss, err := gocvss31.ParseVector(vector) - if err != nil { - return nil, fmt.Errorf("unable to parse CVSS v3.1 vector: %w", err) - } - ex := roundScore(cvss.Exploitability()) - im := roundScore(cvss.Impact()) - return &vulnerability.CvssMetrics{ - BaseScore: roundScore(cvss.BaseScore()), - ExploitabilityScore: &ex, - ImpactScore: &im, - }, nil - case strings.HasPrefix(vector, "CVSS:4.0"): - cvss, err := gocvss40.ParseVector(vector) - if err != nil { - return nil, fmt.Errorf("unable to parse CVSS v4.0 vector: %w", err) - } - // there are no exploitability and impact scores in CVSS v4.0 - return &vulnerability.CvssMetrics{ - BaseScore: roundScore(cvss.Score()), - }, nil - default: - // should be CVSS v2.0 or is invalid - cvss, err := gocvss20.ParseVector(vector) - if err != nil { - return nil, fmt.Errorf("unable to parse CVSS v2 vector: %w", err) - } - ex := roundScore(cvss.Exploitability()) - im := roundScore(cvss.Impact()) - return &vulnerability.CvssMetrics{ - BaseScore: roundScore(cvss.BaseScore()), - ExploitabilityScore: &ex, - ImpactScore: &im, - }, nil - } -} - -// roundScore rounds the score to the nearest tenth based on first.org rounding rules -// see https://www.first.org/cvss/v3.1/specification-document#Appendix-A---Floating-Point-Rounding -func roundScore(score float64) float64 { - intInput := int(math.Round(score * 100000)) - if intInput%10000 == 0 { - return float64(intInput) / 100000.0 - } - return (math.Floor(float64(intInput)/10000.0) + 1) / 10.0 -} - func interpretCVSS(score float64, version string) vulnerability.Severity { switch version { case "2.0": @@ -178,7 +111,7 @@ func toCvss(severities ...Severity) []vulnerability.Cvss { } var usedMetrics vulnerability.CvssMetrics // though the DB has the base score, we parse the vector for all metrics - metrics, err := parseCVSS(cvssSev.Vector) + metrics, err := cvss.ParseMetricsFromVector(cvssSev.Vector) if err != nil { log.WithFields("vector", cvssSev.Vector, "error", err).Warn("unable to parse CVSS vector") continue diff --git a/grype/db/v6/severity_test.go b/grype/db/v6/severity_test.go index e8b3b41f63a..3b943d0f448 100644 --- a/grype/db/v6/severity_test.go +++ b/grype/db/v6/severity_test.go @@ -126,107 +126,6 @@ func TestExtractSeverity(t *testing.T) { } } -func TestParseCVSS(t *testing.T) { - tests := []struct { - name string - vector string - expectedMetrics *vulnerability.CvssMetrics - wantErr require.ErrorAssertionFunc - }{ - { - name: "valid CVSS 2.0", - vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", - expectedMetrics: &vulnerability.CvssMetrics{ - BaseScore: 7.5, - ExploitabilityScore: ptr(10.0), - ImpactScore: ptr(6.5), - }, - }, - { - name: "valid CVSS 3.0", - vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", - expectedMetrics: &vulnerability.CvssMetrics{ - BaseScore: 9.8, - ExploitabilityScore: ptr(3.9), - ImpactScore: ptr(5.9), - }, - }, - { - name: "valid CVSS 3.1", - vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", - expectedMetrics: &vulnerability.CvssMetrics{ - BaseScore: 9.8, - ExploitabilityScore: ptr(3.9), - ImpactScore: ptr(5.9), - }, - }, - { - name: "valid CVSS 4.0", - vector: "CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:N/VI:H/VA:L/SC:L/SI:H/SA:L/MAC:L/MAT:P/MPR:N/S:N/R:A/RE:L/U:Clear", - expectedMetrics: &vulnerability.CvssMetrics{ - BaseScore: 9.1, - }, - }, - { - name: "invalid CVSS 2.0", - vector: "AV:N/AC:INVALID", - wantErr: require.Error, - }, - { - name: "invalid CVSS 3.0", - vector: "CVSS:3.0/AV:INVALID", - wantErr: require.Error, - }, - { - name: "invalid CVSS 3.1", - vector: "CVSS:3.1/AV:INVALID", - wantErr: require.Error, - }, - { - name: "invalid CVSS 4.0", - vector: "CVSS:4.0/AV:INVALID", - wantErr: require.Error, - }, - { - name: "empty vector", - vector: "", - wantErr: require.Error, - }, - { - name: "malformed vector", - vector: "INVALID:VECTOR", - wantErr: require.Error, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.wantErr == nil { - tt.wantErr = require.NoError - } - result, err := parseCVSS(tt.vector) - tt.wantErr(t, err) - if err != nil { - assert.Nil(t, result) - return - } - - require.NotNil(t, result) - assert.Equal(t, tt.expectedMetrics.BaseScore, result.BaseScore, "given vector: %s", tt.vector) - - if tt.expectedMetrics.ExploitabilityScore != nil { - require.NotNil(t, result.ExploitabilityScore) - assert.Equal(t, *tt.expectedMetrics.ExploitabilityScore, *result.ExploitabilityScore, "given vector: %s", tt.vector) - } - - if tt.expectedMetrics.ImpactScore != nil { - require.NotNil(t, result.ImpactScore) - assert.Equal(t, *tt.expectedMetrics.ImpactScore, *result.ImpactScore, "given vector: %s", tt.vector) - } - }) - } -} - func TestExtractSeverities(t *testing.T) { tests := []struct { name string diff --git a/grype/db/v6/status.go b/grype/db/v6/status.go deleted file mode 100644 index ee90524ca65..00000000000 --- a/grype/db/v6/status.go +++ /dev/null @@ -1,39 +0,0 @@ -package v6 - -import "encoding/json" - -type Status struct { - SchemaVersion string `json:"schemaVersion"` - Built Time `json:"built"` - Path string `json:"path"` - Checksum string `json:"checksum"` - Err error `json:"error"` -} - -func (s Status) Status() string { - if s.Err != nil { - return "invalid" - } - return "valid" -} - -func (s Status) MarshalJSON() ([]byte, error) { - errStr := "" - if s.Err != nil { - errStr = s.Err.Error() - } - - return json.Marshal(&struct { - SchemaVersion string `json:"schemaVersion"` - Built Time `json:"built"` - Path string `json:"path"` - Checksum string `json:"checksum"` - Err string `json:"error"` - }{ - SchemaVersion: s.SchemaVersion, - Built: s.Built, - Path: s.Path, - Checksum: s.Checksum, - Err: errStr, - }) -} diff --git a/grype/db/v6/store.go b/grype/db/v6/store.go index dfc39def34c..8a39bb5c678 100644 --- a/grype/db/v6/store.go +++ b/grype/db/v6/store.go @@ -65,17 +65,16 @@ func newStore(cfg Config, empty, writable bool) (*store, error) { } meta, err := metadataStore.GetDBMetadata() - if err != nil { + if err != nil || meta == nil || meta.Model != ModelVersion { // db.Close must be called, or we will get stale reads d, _ := db.DB() if d != nil { _ = d.Close() } - return nil, fmt.Errorf("failed to get db metadata: %w", err) - } - - if meta == nil { - return nil, fmt.Errorf("no DB metadata found") + if err != nil { + return nil, fmt.Errorf("not a v%d database: %w", ModelVersion, err) + } + return nil, fmt.Errorf("not a v%d database", ModelVersion) } dbVersion := newSchemaVerFromDBMetadata(*meta) diff --git a/grype/db/v6/store_test.go b/grype/db/v6/store_test.go index b508d6155a1..16261061a6f 100644 --- a/grype/db/v6/store_test.go +++ b/grype/db/v6/store_test.go @@ -1,10 +1,12 @@ package v6 import ( + "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gorm.io/gorm" ) func TestStoreClose(t *testing.T) { @@ -54,3 +56,23 @@ func TestStoreClose(t *testing.T) { assert.Empty(t, indexes) }) } + +func Test_oldDbV5(t *testing.T) { + s := setupTestStore(t) + require.NoError(t, s.db.Where("true").Delete(&DBMetadata{}).Error) // delete all existing records + require.NoError(t, s.Close()) + s, err := newStore(s.config, false, true) + require.Nil(t, s) + require.ErrorIs(t, err, gorm.ErrRecordNotFound) + require.ErrorContains(t, err, fmt.Sprintf("not a v%d database", ModelVersion)) +} + +func Test_oldDbWithMetadata(t *testing.T) { + s := setupTestStore(t) + require.NoError(t, s.db.Where("true").Model(DBMetadata{}).Update("Model", "5").Error) // old database version + require.NoError(t, s.Close()) + s, err := newStore(s.config, false, true) + require.Nil(t, s) + require.NotErrorIs(t, err, gorm.ErrRecordNotFound) + require.ErrorContains(t, err, fmt.Sprintf("not a v%d database", ModelVersion)) +} diff --git a/grype/db/v6/testutil/server.go b/grype/db/v6/testutil/server.go index 1188e4c2043..69626857db8 100644 --- a/grype/db/v6/testutil/server.go +++ b/grype/db/v6/testutil/server.go @@ -109,10 +109,10 @@ func (s *ServerBuilder) Start() (url string) { case serverSubdir + s.LatestDocFile: latestDoc := *s.LatestDoc latestDoc.Built.Time = s.DBBuildTime - latestDoc.Archive.SchemaVersion = s.DBVersion - latestDoc.Archive.Built.Time = s.DBBuildTime - latestDoc.Archive.Path = archivePath - latestDoc.Archive.Checksum = sha(s.dbContents) + latestDoc.SchemaVersion = s.DBVersion + latestDoc.Built.Time = s.DBBuildTime + latestDoc.Path = archivePath + latestDoc.Checksum = sha(s.dbContents) w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(latestDoc) case serverSubdir + archivePath: diff --git a/grype/db/v6/vulnerability.go b/grype/db/v6/vulnerability.go index 2da7700fc1e..c17f528bea9 100644 --- a/grype/db/v6/vulnerability.go +++ b/grype/db/v6/vulnerability.go @@ -104,7 +104,7 @@ func getRelatedVulnerabilities(vuln *VulnerabilityHandle, affected *AffectedPack cveSet := strset.New() var relatedVulnerabilities []vulnerability.Reference for _, alias := range vuln.BlobValue.Aliases { - if cveSet.Has(alias) { + if cveSet.Has(alias) || strings.EqualFold(vuln.Name, alias) { continue } if !strings.HasPrefix(strings.ToLower(alias), "cve-") { @@ -118,7 +118,7 @@ func getRelatedVulnerabilities(vuln *VulnerabilityHandle, affected *AffectedPack } if affected != nil { for _, cve := range affected.CVEs { - if cveSet.Has(cve) { + if cveSet.Has(cve) || strings.EqualFold(vuln.Name, cve) { continue } if !strings.HasPrefix(strings.ToLower(cve), "cve-") { @@ -146,39 +146,10 @@ func getPackageQualifiers(affected *AffectedPackageBlob) []qualifier.Qualifier { // //nolint:funlen func MimicV5Namespace(vuln *VulnerabilityHandle, affected *AffectedPackageHandle) string { - if affected == nil { // for CPE matches - return v5NvdNamespace - } - switch vuln.Provider.ID { - case "nvd": - return v5NvdNamespace - case "github": - language := affected.Package.Ecosystem - // normalize from purl type, github ecosystem types, and vunnel mappings - switch strings.ToLower(language) { - case "golang", string(pkg.GoModulePkg): - language = "go" - case "composer", string(pkg.PhpComposerPkg): - language = "php" - case "cargo", string(pkg.RustPkg): - language = "rust" - case "pub", string(pkg.DartPubPkg): - language = "dart" - case "nuget", string(pkg.DotnetPkg): - language = "dotnet" - case "maven", string(pkg.JavaPkg), string(pkg.JenkinsPluginPkg): - language = "java" - case "swifturl", string(pkg.SwiplPackPkg), string(pkg.SwiftPkg): - language = "swift" - case "node", string(pkg.NpmPkg): - language = "javascript" - case "pypi", "pip", string(pkg.PythonPkg): - language = "python" - case "rubygems", string(pkg.GemPkg): - language = "ruby" - } - return fmt.Sprintf("github:language:%s", language) + if affected == nil || affected.Package == nil { // for CPE matches + return fmt.Sprintf("%s:cpe", vuln.Provider.ID) } + if affected.OperatingSystem != nil { // distro family fixes family := affected.OperatingSystem.Name @@ -229,6 +200,40 @@ func MimicV5Namespace(vuln *VulnerabilityHandle, affected *AffectedPackageHandle return fmt.Sprintf("%s:distro:%s:%s", pr, family, ver) } + + if affected.Package != nil { + language := affected.Package.Ecosystem + // normalize from purl type, github ecosystem types, and vunnel mappings + switch strings.ToLower(language) { + case "golang", string(pkg.GoModulePkg): + language = "go" + case "composer", string(pkg.PhpComposerPkg): + language = "php" + case "cargo", string(pkg.RustPkg): + language = "rust" + case "pub", string(pkg.DartPubPkg): + language = "dart" + case "nuget", string(pkg.DotnetPkg): + language = "dotnet" + case "maven", string(pkg.JavaPkg), string(pkg.JenkinsPluginPkg): + language = "java" + case "swifturl", string(pkg.SwiplPackPkg), string(pkg.SwiftPkg): + language = "swift" + case "node", string(pkg.NpmPkg): + language = "javascript" + case "pypi", "pip", string(pkg.PythonPkg): + language = "python" + case "rubygems", string(pkg.GemPkg): + language = "ruby" + case "msrc", string(pkg.KbPkg): // msrc packages were previously modelled as distro + return fmt.Sprintf("%s:distro:windows:%s", vuln.Provider.ID, affected.Package.Name) + case "": // CPE + return fmt.Sprintf("%s:cpe", vuln.Provider.ID) + } + return fmt.Sprintf("%s:language:%s", vuln.Provider.ID, language) + } + + // this shouldn't happen and is not a valid v5 namespace, but some information is better than none return vuln.Provider.ID } diff --git a/grype/db/v6/vulnerability_provider.go b/grype/db/v6/vulnerability_provider.go index aa5eff83c48..6e012daac38 100644 --- a/grype/db/v6/vulnerability_provider.go +++ b/grype/db/v6/vulnerability_provider.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "strings" + "time" "github.com/hashicorp/go-multierror" "github.com/iancoleman/strcase" @@ -21,6 +22,11 @@ import ( syftPkg "github.com/anchore/syft/syft/pkg" ) +var ( + _ vulnerability.Provider = (*vulnerabilityProvider)(nil) + _ vulnerability.StoreMetadataProvider = (*vulnerabilityProvider)(nil) +) + func NewVulnerabilityProvider(rdr Reader) vulnerability.Provider { return &vulnerabilityProvider{ reader: rdr, @@ -36,11 +42,11 @@ var _ interface { } = (*vulnerabilityProvider)(nil) // Deprecated: vulnerability.Vulnerability objects now have metadata included -func (s vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) { +func (vp vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) { vuln, ok := ref.Internal.(*VulnerabilityHandle) if !ok { var err error - vuln, err = s.fetchVulnerability(ref) + vuln, err = vp.fetchVulnerability(ref) if err != nil { return nil, err } @@ -56,18 +62,18 @@ func (s vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference }, nil } - return s.getVulnerabilityMetadata(vuln, ref.Namespace) + return vp.getVulnerabilityMetadata(vuln, ref.Namespace) } -func (s vulnerabilityProvider) getVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string) (*vulnerability.Metadata, error) { +func (vp vulnerabilityProvider) getVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string) (*vulnerability.Metadata, error) { cves := getCVEs(vuln) - kevs, err := s.fetchKnownExploited(cves) + kevs, err := vp.fetchKnownExploited(cves) if err != nil { log.WithFields("id", vuln.Name, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch known exploited from vulnerability") } - epss, err := s.fetchEpss(cves) + epss, err := vp.fetchEpss(cves) if err != nil { log.WithFields("id", vuln.Name, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch epss from vulnerability") } @@ -83,20 +89,14 @@ func newVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string, kevs sev, cvss, err := extractSeverities(vuln) if err != nil { log.WithFields("id", vuln.Name, "vulnerability", vuln.String()).Debug("unable to extract severity from vulnerability") - return &vulnerability.Metadata{ - ID: vuln.Name, - DataSource: strings.Split(namespace, ":")[0], - Namespace: namespace, - Severity: toSeverityString(vulnerability.UnknownSeverity), - }, nil } return &vulnerability.Metadata{ ID: vuln.Name, - DataSource: vuln.Provider.ID, + DataSource: firstReferenceURL(vuln), Namespace: namespace, Severity: toSeverityString(sev), - URLs: toURLs(vuln), + URLs: lastReferenceURLs(vuln), Description: vuln.BlobValue.Description, Cvss: cvss, KnownExploited: kevs, @@ -104,9 +104,29 @@ func newVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string, kevs }, nil } -func (s vulnerabilityProvider) fetchVulnerability(ref vulnerability.Reference) (*VulnerabilityHandle, error) { +func (vp vulnerabilityProvider) DataProvenance() (map[string]vulnerability.DataProvenance, error) { + providers, err := vp.reader.AllProviders() + if err != nil { + return nil, err + } + dps := make(map[string]vulnerability.DataProvenance) + + for _, p := range providers { + var date time.Time + if p.DateCaptured != nil { + date = *p.DateCaptured + } + dps[p.ID] = vulnerability.DataProvenance{ + DateCaptured: date, + InputDigest: p.InputDigest, + } + } + return dps, nil +} + +func (vp vulnerabilityProvider) fetchVulnerability(ref vulnerability.Reference) (*VulnerabilityHandle, error) { provider := strings.Split(ref.Namespace, ":")[0] - vulns, err := s.reader.GetVulnerabilities(&VulnerabilitySpecifier{Name: ref.ID, Providers: []string{provider}}, &GetVulnerabilityOptions{Preload: true}) + vulns, err := vp.reader.GetVulnerabilities(&VulnerabilitySpecifier{Name: ref.ID, Providers: []string{provider}}, &GetVulnerabilityOptions{Preload: true}) if err != nil { return nil, err } @@ -116,11 +136,11 @@ func (s vulnerabilityProvider) fetchVulnerability(ref vulnerability.Reference) ( return nil, nil } -func (s vulnerabilityProvider) fetchKnownExploited(cves []string) ([]vulnerability.KnownExploited, error) { +func (vp vulnerabilityProvider) fetchKnownExploited(cves []string) ([]vulnerability.KnownExploited, error) { var out []vulnerability.KnownExploited var errs error for _, cve := range cves { - kevs, err := s.reader.GetKnownExploitedVulnerabilities(cve) + kevs, err := vp.reader.GetKnownExploitedVulnerabilities(cve) if err != nil { errs = multierror.Append(errs, err) continue @@ -143,11 +163,11 @@ func (s vulnerabilityProvider) fetchKnownExploited(cves []string) ([]vulnerabili return out, errs } -func (s vulnerabilityProvider) fetchEpss(cves []string) ([]vulnerability.EPSS, error) { +func (vp vulnerabilityProvider) fetchEpss(cves []string) ([]vulnerability.EPSS, error) { var out []vulnerability.EPSS var errs error for _, cve := range cves { - entries, err := s.reader.GetEpss(cve) + entries, err := vp.reader.GetEpss(cve) if err != nil { errs = multierror.Append(errs, err) continue @@ -164,16 +184,16 @@ func (s vulnerabilityProvider) fetchEpss(cves []string) ([]vulnerability.EPSS, e return out, errs } -func (s vulnerabilityProvider) PackageSearchNames(p pkg.Package) []string { +func (vp vulnerabilityProvider) PackageSearchNames(p pkg.Package) []string { return name.PackageNames(p) } -func (s vulnerabilityProvider) Close() error { - return s.reader.(io.Closer).Close() +func (vp vulnerabilityProvider) Close() error { + return vp.reader.(io.Closer).Close() } //nolint:funlen,gocognit,gocyclo -func (s vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { +func (vp vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { if err := search.ValidateCriteria(criteria); err != nil { return nil, err } @@ -202,11 +222,20 @@ func (s vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Cri pkgSpec = &PackageSpecifier{} } // the v6 store normalizes ecosystems around the syft package type, so that field is preferred - if c.PackageType != "" { - pkgSpec.Ecosystem = string(c.PackageType) + switch { + case c.PackageType != "" && c.PackageType != syftPkg.UnknownPkg: + // prefer to match by a non-blank, known package type pkgType = c.PackageType - } else { + pkgSpec.Ecosystem = string(c.PackageType) + case c.Language != "": + // if there's no known package type, but there is a non-blank language + // try that. pkgSpec.Ecosystem = string(c.Language) + case c.PackageType == syftPkg.UnknownPkg: + // if language is blank, and package type is explicitly "UnknownPkg" and not + // just blank, use that. + pkgType = c.PackageType + pkgSpec.Ecosystem = string(c.PackageType) } applied = true case *search.IDCriteria: @@ -230,9 +259,11 @@ func (s vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Cri case *search.DistroCriteria: for _, d := range c.Distros { osSpecs = append(osSpecs, &OSSpecifier{ - Name: d.Name(), - MajorVersion: d.MajorVersion(), - MinorVersion: d.MinorVersion(), + Name: d.Name(), + MajorVersion: d.MajorVersion(), + MinorVersion: d.MinorVersion(), + RemainingVersion: d.RemainingVersion(), + LabelVersion: d.Codename, }) } applied = true @@ -263,7 +294,7 @@ func (s vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Cri var affectedCPEs []AffectedCPEHandle if pkgSpec != nil || len(vulnSpecs) > 0 { - affectedPackages, err = s.reader.GetAffectedPackages(pkgSpec, &GetAffectedPackageOptions{ + affectedPackages, err = vp.reader.GetAffectedPackages(pkgSpec, &GetAffectedPackageOptions{ OSs: osSpecs, Vulnerabilities: vulnSpecs, PreloadBlob: true, @@ -279,13 +310,13 @@ func (s vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Cri affectedPackages = filterAffectedPackageVersions(versionMatcher, affectedPackages) // after filtering, read vulnerability data - if err = fillAffectedPackageHandles(s.reader, ptrs(affectedPackages)); err != nil { + if err = fillAffectedPackageHandles(vp.reader, ptrs(affectedPackages)); err != nil { return nil, err } } if cpeSpec != nil { - affectedCPEs, err = s.reader.GetAffectedCPEs(cpeSpec, &GetAffectedCPEOptions{ + affectedCPEs, err = vp.reader.GetAffectedCPEs(cpeSpec, &GetAffectedCPEOptions{ Vulnerabilities: vulnSpecs, PreloadBlob: true, }) @@ -296,19 +327,19 @@ func (s vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Cri affectedCPEs = filterAffectedCPEVersions(versionMatcher, affectedCPEs, cpeSpec) // after filtering, read vulnerability data - if err = fillAffectedCPEHandles(s.reader, ptrs(affectedCPEs)); err != nil { + if err = fillAffectedCPEHandles(vp.reader, ptrs(affectedCPEs)); err != nil { return nil, err } } // fill complete vulnerabilities for this set -- these should have already had all properties lazy loaded - vulns, err := s.toVulnerabilities(affectedPackages, affectedCPEs) + vulns, err := vp.toVulnerabilities(affectedPackages, affectedCPEs) if err != nil { return nil, err } // filter vulnerabilities by any remaining criteria such as ByQualifiedPackages - vulns, err = s.filterVulnerabilities(vulns, remainingCriteria...) + vulns, err = vp.filterVulnerabilities(vulns, remainingCriteria...) if err != nil { return nil, err } @@ -319,7 +350,7 @@ func (s vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Cri return out, nil } -func (s vulnerabilityProvider) filterVulnerabilities(vulns []vulnerability.Vulnerability, criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { +func (vp vulnerabilityProvider) filterVulnerabilities(vulns []vulnerability.Vulnerability, criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { isMatch := func(v vulnerability.Vulnerability) (bool, error) { for _, c := range criteria { if _, ok := c.(search.VersionConstraintMatcher); ok { @@ -354,7 +385,7 @@ func (s vulnerabilityProvider) filterVulnerabilities(vulns []vulnerability.Vulne } // toVulnerabilities takes fully-filled handles and returns all vulnerabilities from them -func (s vulnerabilityProvider) toVulnerabilities(packageHandles []AffectedPackageHandle, cpeHandles []AffectedCPEHandle) ([]vulnerability.Vulnerability, error) { //nolint:funlen,gocognit +func (vp vulnerabilityProvider) toVulnerabilities(packageHandles []AffectedPackageHandle, cpeHandles []AffectedCPEHandle) ([]vulnerability.Vulnerability, error) { //nolint:funlen,gocognit var out []vulnerability.Vulnerability metadataByCVE := make(map[string]*vulnerability.Metadata) @@ -368,7 +399,7 @@ func (s vulnerabilityProvider) toVulnerabilities(packageHandles []AffectedPackag return metadata, nil } - metadata, err := s.getVulnerabilityMetadata(vuln, namespace) + metadata, err := vp.getVulnerabilityMetadata(vuln, namespace) if err != nil { return nil, err } @@ -390,7 +421,7 @@ func (s vulnerabilityProvider) toVulnerabilities(packageHandles []AffectedPackag continue } - meta, err := getMetadata(packageHandle.Vulnerability, v.Reference.Namespace) + meta, err := getMetadata(packageHandle.Vulnerability, v.Namespace) if err != nil { log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability") } else { @@ -413,7 +444,7 @@ func (s vulnerabilityProvider) toVulnerabilities(packageHandles []AffectedPackag continue } - meta, err := getMetadata(c.Vulnerability, v.Reference.Namespace) + meta, err := getMetadata(c.Vulnerability, v.Namespace) if err != nil { log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability") } else { @@ -522,9 +553,21 @@ func toSeverityString(sev vulnerability.Severity) string { return strcase.ToCamel(sev.String()) } -func toURLs(vuln *VulnerabilityHandle) []string { - var out []string +// returns the first reference url to populate the DataSource +func firstReferenceURL(vuln *VulnerabilityHandle) string { for _, v := range vuln.BlobValue.References { + return v.URL + } + return "" +} + +// skip the first reference URL and return the remainder to populate the URLs +func lastReferenceURLs(vuln *VulnerabilityHandle) []string { + var out []string + for i, v := range vuln.BlobValue.References { + if i == 0 { + continue + } out = append(out, v.URL) } return out diff --git a/grype/db/v6/vulnerability_provider_mocks_test.go b/grype/db/v6/vulnerability_provider_mocks_test.go index d2a76838be1..339fe650fc5 100644 --- a/grype/db/v6/vulnerability_provider_mocks_test.go +++ b/grype/db/v6/vulnerability_provider_mocks_test.go @@ -29,7 +29,7 @@ func testVulnerabilityProvider(t *testing.T) vulnerability.Provider { aWeekAgo := time.Now().Add(-7 * 24 * time.Hour) twoWeeksAgo := time.Now().Add(-14 * 24 * time.Hour) - prov := &Provider{ + debianProvider := &Provider{ ID: "debian", Version: "1", Processor: "debian-processor", @@ -37,6 +37,14 @@ func testVulnerabilityProvider(t *testing.T) vulnerability.Provider { InputDigest: hex.EncodeToString([]byte("debian")), } + nvdProvider := &Provider{ + ID: "nvd", + Version: "1", + Processor: "nvd-processor", + DateCaptured: &aDayAgo, + InputDigest: hex.EncodeToString([]byte("nvd")), + } + v5vulns := []v5.Vulnerability{ // neutron { @@ -103,6 +111,13 @@ func testVulnerabilityProvider(t *testing.T) vulnerability.Provider { "cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", // shouldn't match on this }, }, + { + PackageName: "Newtonsoft.Json", + Namespace: "github:language:dotnet", + ID: "GHSA-5crp-9r3c-p9vr", + VersionFormat: "unknown", + VersionConstraint: "<13.0.1", + }, // poison the well! this is not a valid entry, but we want the matching process to survive and find other good results... { PackageName: "activerecord", @@ -118,10 +133,12 @@ func testVulnerabilityProvider(t *testing.T) vulnerability.Provider { for _, v := range v5vulns { var os *OperatingSystem + prov := nvdProvider switch v.Namespace { case "nvd:cpe": case "debian:distro:debian:8": + prov = debianProvider os = &OperatingSystem{ Name: "debian", MajorVersion: "8", diff --git a/grype/db/v6/vulnerability_provider_test.go b/grype/db/v6/vulnerability_provider_test.go index 010ec973215..73b451ced82 100644 --- a/grype/db/v6/vulnerability_provider_test.go +++ b/grype/db/v6/vulnerability_provider_test.go @@ -17,6 +17,7 @@ import ( "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" + syftPkg "github.com/anchore/syft/syft/pkg" ) func Test_FindVulnerabilitiesByDistro(t *testing.T) { @@ -46,10 +47,10 @@ func Test_FindVulnerabilitiesByDistro(t *testing.T) { Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-1", - DataSource: "debian", + DataSource: "http://somewhere/CVE-2014-fake-1", Namespace: "debian:distro:debian:8", Severity: "High", - URLs: []string{"http://somewhere/CVE-2014-fake-1"}, + URLs: nil, Description: "CVE-2014-fake-1-description", }, }, @@ -65,10 +66,10 @@ func Test_FindVulnerabilitiesByDistro(t *testing.T) { Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2013-fake-2", - DataSource: "debian", + DataSource: "http://somewhere/CVE-2013-fake-2", Namespace: "debian:distro:debian:8", Severity: "High", - URLs: []string{"http://somewhere/CVE-2013-fake-2"}, + URLs: nil, Description: "CVE-2013-fake-2-description", }, }, @@ -123,10 +124,10 @@ func Test_FindVulnerabilitiesByCPE(t *testing.T) { Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-4", - DataSource: "debian", + DataSource: "http://somewhere/CVE-2014-fake-4", Namespace: "nvd:cpe", Severity: "High", - URLs: []string{"http://somewhere/CVE-2014-fake-4"}, + URLs: nil, Description: "CVE-2014-fake-4-description", }, }, @@ -150,10 +151,10 @@ func Test_FindVulnerabilitiesByCPE(t *testing.T) { Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-4", - DataSource: "debian", + DataSource: "http://somewhere/CVE-2014-fake-4", Namespace: "nvd:cpe", Severity: "High", - URLs: []string{"http://somewhere/CVE-2014-fake-4"}, + URLs: nil, Description: "CVE-2014-fake-4-description", }, }, @@ -177,10 +178,10 @@ func Test_FindVulnerabilitiesByCPE(t *testing.T) { Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-3", - DataSource: "debian", + DataSource: "http://somewhere/CVE-2014-fake-3", Namespace: "nvd:cpe", Severity: "High", - URLs: []string{"http://somewhere/CVE-2014-fake-3"}, + URLs: nil, Description: "CVE-2014-fake-3-description", }, }, @@ -198,10 +199,10 @@ func Test_FindVulnerabilitiesByCPE(t *testing.T) { Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-4", - DataSource: "debian", + DataSource: "http://somewhere/CVE-2014-fake-4", Namespace: "nvd:cpe", Severity: "High", - URLs: []string{"http://somewhere/CVE-2014-fake-4"}, + URLs: nil, Description: "CVE-2014-fake-4-description", }, }, @@ -270,10 +271,10 @@ func Test_FindVulnerabilitiesByByID(t *testing.T) { Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-1", - DataSource: "debian", + DataSource: "http://somewhere/CVE-2014-fake-1", Namespace: "debian:distro:debian:8", Severity: "High", - URLs: []string{"http://somewhere/CVE-2014-fake-1"}, + URLs: nil, Description: "CVE-2014-fake-1-description", }, }, @@ -303,6 +304,151 @@ func Test_FindVulnerabilitiesByByID(t *testing.T) { require.Empty(t, actual) } +func Test_FindVulnerabilitiesByEcosystem_UnknownPackageType(t *testing.T) { + tests := []struct { + name string + packageName string + packageType syftPkg.Type + language syftPkg.Language + expectedIDs []string + }{ + { + name: "known package type", + packageName: "Newtonsoft.Json", + packageType: syftPkg.DotnetPkg, + language: syftPkg.Java, // deliberately wrong to prove we're using package type + expectedIDs: []string{"GHSA-5crp-9r3c-p9vr"}, + }, + { + name: "unknown package type, known language", + packageName: "Newtonsoft.Json", + packageType: syftPkg.UnknownPkg, + language: syftPkg.Dotnet, + expectedIDs: []string{"GHSA-5crp-9r3c-p9vr"}, + }, + { + name: "unknown package type, unknown language", + packageName: "Newtonsoft.Json", + packageType: syftPkg.UnknownPkg, + language: syftPkg.UnknownLanguage, + // The vuln GHSA-5crp-9r3c-p9vr is specifically associated + // with the dotnet ecosystem, so it should not be returned here. + // In a real search for UnknownPkg + UnknownLanguage, there should + // be a separate search.ByCPE run that _does_ return it. + expectedIDs: []string{}, + }, + } + provider := testVulnerabilityProvider(t) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := provider.FindVulnerabilities( + search.ByEcosystem(test.language, test.packageType), + search.ByPackageName(test.packageName), + ) + require.NoError(t, err) + actualIDs := make([]string, len(actual)) + for idx, vuln := range actual { + actualIDs[idx] = vuln.ID + } + if d := cmp.Diff(test.expectedIDs, actualIDs); d != "" { + t.Errorf("diff: %+v", d) + } + }) + } +} + +func Test_DataSource(t *testing.T) { + tests := []struct { + name string + vuln VulnerabilityHandle + expected vulnerability.Metadata + }{ + { + name: "no reference urls", + vuln: VulnerabilityHandle{ + BlobValue: &VulnerabilityBlob{ + References: nil, + }, + }, + expected: vulnerability.Metadata{ + DataSource: "", + URLs: nil, + }, + }, + { + name: "one reference url", + vuln: VulnerabilityHandle{ + BlobValue: &VulnerabilityBlob{ + References: []Reference{ + { + URL: "url1", + }, + }, + }, + }, + expected: vulnerability.Metadata{ + DataSource: "url1", + URLs: nil, + }, + }, + { + name: "two reference urls", + vuln: VulnerabilityHandle{ + BlobValue: &VulnerabilityBlob{ + References: []Reference{ + { + URL: "url1", + }, + { + URL: "url2", + }, + }, + }, + }, + expected: vulnerability.Metadata{ + DataSource: "url1", + URLs: []string{"url2"}, + }, + }, + { + name: "many reference urls", + vuln: VulnerabilityHandle{ + BlobValue: &VulnerabilityBlob{ + References: []Reference{ + { + URL: "url4", + }, + { + URL: "url3", + }, + { + URL: "url2", + }, + { + URL: "url1", + }, + }, + }, + }, + expected: vulnerability.Metadata{ + DataSource: "url4", + URLs: []string{"url3", "url2", "url1"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newVulnerabilityMetadata(&tt.vuln, "", nil, nil) + got.Severity = "" + require.NoError(t, err) + if diff := cmp.Diff(&tt.expected, got, cmpOpts()...); diff != "" { + t.Fatal(diff) + } + }) + } +} + func cmpOpts() []cmp.Option { return []cmp.Option{ // globally ignore unexported -- these are unexported structs we cannot reference here to use cmpopts.IgnoreUnexported diff --git a/grype/db/v6/vulnerability_test.go b/grype/db/v6/vulnerability_test.go index 934ab2f7ed8..5335a3db626 100644 --- a/grype/db/v6/vulnerability_test.go +++ b/grype/db/v6/vulnerability_test.go @@ -6,6 +6,9 @@ import ( "unicode" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/vulnerability" ) func TestV5Namespace(t *testing.T) { @@ -135,12 +138,13 @@ func TestV5Namespace(t *testing.T) { // +--------------------------------------+ type testCase struct { - name string - provider string // from Providers.id - ecosystem string // only used when provider is "github" - osName string // only used for OS-based providers - osVersion string // only used for OS-based providers - expected string + name string + provider string // from Providers.id + ecosystem string // only used when provider non-os provider + packageName string // only used for msrc + osName string // only used for OS-based providers + osVersion string // only used for OS-based providers + expected string // } tests := []testCase{ @@ -431,6 +435,39 @@ func TestV5Namespace(t *testing.T) { osVersion: "9.3.1", expected: "oracle:distro:oraclelinux:9", }, + // msrc is modeled as a distro for v5 but is just a package in v6 + { + name: "microsoft msrc-kb", + provider: "msrc", + ecosystem: "msrc-kb", + packageName: "10012", + expected: "msrc:distro:windows:10012", + }, + + // new provider existing ecosystem + { + name: "grizzly go-module", + provider: "grizzly", + ecosystem: "go-module", + expected: "grizzly:language:go", + }, + + // new provider new ecosystem + { + name: "armadillo pizza", + provider: "armadillo", + ecosystem: "pizza", + expected: "armadillo:language:pizza", + }, + + // new OS + { + name: "gothmog", + provider: "gothmog", + osName: "gothmoglinux", + osVersion: "zzzzzz11123", + expected: "gothmog:distro:gothmoglinux:zzzzzz11123", + }, } for _, tt := range tests { @@ -440,6 +477,7 @@ func TestV5Namespace(t *testing.T) { ID: tt.provider, }, } + pkg := &AffectedPackageHandle{} if tt.osName != "" { @@ -454,11 +492,14 @@ func TestV5Namespace(t *testing.T) { MinorVersion: minor, LabelVersion: label, } - } - - if tt.provider == "github" { + pkg.Package = &Package{ + Name: "os-package", + Ecosystem: "os-ecosystem", + } + } else if tt.ecosystem != "" { pkg.Package = &Package{ Ecosystem: tt.ecosystem, + Name: tt.packageName, } } @@ -468,6 +509,68 @@ func TestV5Namespace(t *testing.T) { } } +func Test_getRelatedVulnerabilities(t *testing.T) { + tests := []struct { + name string + vuln VulnerabilityHandle + affected AffectedPackageBlob + expected []string + }{ + { + name: "GHSA with related CVEs", + vuln: VulnerabilityHandle{ + Name: "GHSA-1234", + BlobValue: &VulnerabilityBlob{ + Aliases: []string{"CVE-2024-1"}, + }, + }, + affected: AffectedPackageBlob{ + CVEs: []string{"CVE-2024-2", "CVE-2024-3"}, + }, + expected: []string{"CVE-2024-1", "CVE-2024-2", "CVE-2024-3"}, + }, + { + name: "CVE with related CVEs", + vuln: VulnerabilityHandle{ + Name: "CVE-2024-1234", + BlobValue: &VulnerabilityBlob{ + Aliases: []string{"CVE-2024-1"}, + }, + }, + affected: AffectedPackageBlob{ + CVEs: []string{"CVE-2024-2", "CVE-2024-3"}, + }, + expected: []string{"CVE-2024-1", "CVE-2024-2", "CVE-2024-3"}, + }, + { + name: "CVE with related CVEs and self", + vuln: VulnerabilityHandle{ + Name: "CVE-2024-1234", + BlobValue: &VulnerabilityBlob{ + Aliases: []string{"CVE-2024-1", "CVE-2024-1234"}, + }, + }, + affected: AffectedPackageBlob{ + CVEs: []string{"CVE-2024-2", "CVE-2024-1234"}, + }, + expected: []string{"CVE-2024-1", "CVE-2024-2"}, // does not include "CVE-2024-1234" + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getRelatedVulnerabilities(&tt.vuln, &tt.affected) + var expected []vulnerability.Reference + for _, name := range tt.expected { + expected = append(expected, vulnerability.Reference{ + ID: name, + Namespace: v5NvdNamespace, + }) + } + require.ElementsMatch(t, expected, got) + }) + } +} + func majorMinorPatch(ver string) (string, string, string) { if !unicode.IsDigit(rune(ver[0])) { return "", "", "" diff --git a/grype/deprecated.go b/grype/deprecated.go index fc8ec72b29f..050974fef36 100644 --- a/grype/deprecated.go +++ b/grype/deprecated.go @@ -1,6 +1,7 @@ package grype import ( + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/pkg" @@ -8,7 +9,6 @@ import ( "github.com/anchore/grype/internal/log" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" ) @@ -33,7 +33,7 @@ func FindVulnerabilities(store vulnerability.Provider, userImageStr string, scop } // TODO: deprecated, will remove before v1.0.0 -func FindVulnerabilitiesForPackage(store vulnerability.Provider, d *linux.Release, matchers []match.Matcher, packages []pkg.Package) match.Matches { +func FindVulnerabilitiesForPackage(store vulnerability.Provider, d *distro.Distro, matchers []match.Matcher, packages []pkg.Package) match.Matches { exclusionProvider, _ := store.(match.ExclusionProvider) // TODO v5 is an exclusion provider, but v6 is not runner := VulnerabilityMatcher{ VulnerabilityProvider: store, diff --git a/grype/distro/distro.go b/grype/distro/distro.go index 17ac8dbb054..50cf68855c8 100644 --- a/grype/distro/distro.go +++ b/grype/distro/distro.go @@ -6,37 +6,91 @@ import ( hashiVer "github.com/hashicorp/go-version" + "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/linux" ) // Distro represents a Linux Distribution. type Distro struct { - Type Type - Version *hashiVer.Version - RawVersion string - IDLike []string + Type Type + Version string + Codename string + IDLike []string + + // fields populated in the constructor + + major string + minor string + remaining string } // New creates a new Distro object populated with the given values. -func New(t Type, version string, idLikes ...string) (*Distro, error) { - var verObj *hashiVer.Version - var err error - +func New(t Type, version, label string, idLikes ...string) (*Distro, error) { + var major, minor, remaining string if version != "" { - verObj, err = hashiVer.NewVersion(version) - if err != nil { - return nil, fmt.Errorf("unable to parse version: %w", err) + // if starts with a digit, then assume it's a version and extract the major, minor, and remaining versions + if version[0] >= '0' && version[0] <= '9' { + // extract the major, minor, and remaining versions + parts := strings.Split(version, ".") + if len(parts) > 0 { + major = parts[0] + if len(parts) > 1 { + minor = parts[1] + } + if len(parts) > 2 { + remaining = strings.Join(parts[2:], ".") + } + } + } + } + + for i := range idLikes { + typ, ok := IDMapping[strings.TrimSpace(idLikes[i])] + if ok { + idLikes[i] = typ.String() } } return &Distro{ - Type: t, - Version: verObj, - RawVersion: version, - IDLike: idLikes, + Type: t, + major: major, + minor: minor, + remaining: remaining, + Version: version, + Codename: label, + IDLike: idLikes, }, nil } +// NewFromNameVersion creates a new Distro object derived from the provided name and version +func NewFromNameVersion(name, version string) (*Distro, error) { + var codename string + + // if there are no digits in the version, it is likely a codename + if !strings.ContainsAny(version, "0123456789") { + codename = version + version = "" + } + + typ := IDMapping[name] + if typ == "" { + typ = Type(name) + } + return New(typ, version, codename, string(typ)) +} + +// FromRelease attempts to get a distro from the linux release, only logging any errors +func FromRelease(linuxRelease *linux.Release) *Distro { + if linuxRelease == nil { + return nil + } + d, err := NewFromRelease(*linuxRelease) + if err != nil { + log.WithFields("error", err).Warn("unable to create distro from linux distribution") + } + return d +} + // NewFromRelease creates a new Distro object derived from a syft linux.Release object. func NewFromRelease(release linux.Release) (*Distro, error) { t := TypeFromRelease(release) @@ -51,21 +105,18 @@ func NewFromRelease(release linux.Release) (*Distro, error) { continue } - if _, err := hashiVer.NewVersion(version); err == nil { + _, err := hashiVer.NewVersion(version) + if err == nil { selectedVersion = version break } } - if t == Debian && release.VersionID == "" && release.Version == "" && strings.Contains(release.PrettyName, "sid") { - return &Distro{ - Type: t, - RawVersion: "unstable", - IDLike: release.IDLike, - }, nil + if selectedVersion == "" { + selectedVersion = release.VersionID } - return New(t, selectedVersion, release.IDLike...) + return New(t, selectedVersion, release.VersionCodename, release.IDLike...) } func (d Distro) Name() string { @@ -74,50 +125,33 @@ func (d Distro) Name() string { // MajorVersion returns the major version value from the pseudo-semantically versioned distro version value. func (d Distro) MajorVersion() string { - if d.Version == nil { - return strings.Split(d.RawVersion, ".")[0] - } - return fmt.Sprintf("%d", d.Version.Segments()[0]) + return d.major } // MinorVersion returns the minor version value from the pseudo-semantically versioned distro version value. func (d Distro) MinorVersion() string { - if d.Version == nil { - parts := strings.Split(d.RawVersion, ".") - if len(parts) > 1 { - return parts[1] - } - return "" - } - parts := d.Version.Segments() - if len(parts) > 1 { - return fmt.Sprintf("%d", parts[1]) - } - return "" + return d.minor } -// FullVersion returns the original user version value. -func (d Distro) FullVersion() string { - return d.RawVersion +func (d Distro) RemainingVersion() string { + return d.remaining } // String returns a human-friendly representation of the Linux distribution. func (d Distro) String() string { versionStr := "(version unknown)" - if d.RawVersion != "" { - versionStr = d.RawVersion + if d.Version != "" { + versionStr = d.Version + } else if d.Codename != "" { + versionStr = d.Codename } return fmt.Sprintf("%s %s", d.Type, versionStr) } -func (d Distro) IsRolling() bool { - return d.Type == Wolfi || d.Type == Chainguard || d.Type == ArchLinux || d.Type == Gentoo -} - // Unsupported Linux distributions func (d Distro) Disabled() bool { - switch { - case d.Type == ArchLinux: + switch d.Type { + case ArchLinux: return true default: return false diff --git a/grype/distro/distro_test.go b/grype/distro/distro_test.go index c18be758fa1..c9ca73e6972 100644 --- a/grype/distro/distro_test.go +++ b/grype/distro/distro_test.go @@ -3,6 +3,8 @@ package distro import ( "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,12 +16,12 @@ import ( func Test_NewDistroFromRelease(t *testing.T) { tests := []struct { - name string - release linux.Release - expectedVersion string - expectedRawVersion string - expectedType Type - expectErr bool + name string + release linux.Release + expected *Distro + minor string + major string + expectErr require.ErrorAssertionFunc }{ { name: "go case: derive version from version-id", @@ -27,10 +29,15 @@ func Test_NewDistroFromRelease(t *testing.T) { ID: "centos", VersionID: "8", Version: "7", + IDLike: []string{"rhel"}, }, - expectedType: CentOS, - expectedRawVersion: "8", - expectedVersion: "8.0.0", + expected: &Distro{ + Type: CentOS, + Version: "8", + IDLike: []string{"redhat"}, + }, + major: "8", + minor: "", }, { name: "fallback to release name when release id is missing", @@ -38,9 +45,12 @@ func Test_NewDistroFromRelease(t *testing.T) { Name: "windows", VersionID: "8", }, - expectedType: Windows, - expectedRawVersion: "8", - expectedVersion: "8.0.0", + expected: &Distro{ + Type: Windows, + Version: "8", + }, + major: "8", + minor: "", }, { name: "fallback to version when version-id missing", @@ -48,16 +58,22 @@ func Test_NewDistroFromRelease(t *testing.T) { ID: "centos", Version: "8", }, - expectedType: CentOS, - expectedRawVersion: "8", - expectedVersion: "8.0.0", + expected: &Distro{ + Type: CentOS, + Version: "8", + }, + major: "8", + minor: "", }, { - name: "missing version results in error", + // this enables matching on multiple OS versions at once + name: "missing version or label version is allowed", release: linux.Release{ ID: "centos", }, - expectedType: CentOS, + expected: &Distro{ + Type: CentOS, + }, }, { name: "bogus distro type results in error", @@ -65,7 +81,7 @@ func Test_NewDistroFromRelease(t *testing.T) { ID: "bogosity", VersionID: "8", }, - expectErr: true, + expectErr: require.Error, }, { // syft -o json debian:testing | jq .distro @@ -78,9 +94,12 @@ func Test_NewDistroFromRelease(t *testing.T) { VersionCodename: "trixie", Name: "Debian GNU/Linux", }, - expectedType: Debian, - expectedRawVersion: "unstable", - expectedVersion: "", + expected: &Distro{ + Type: Debian, + Codename: "trixie", + }, + major: "", + minor: "", }, { name: "azure linux 3", @@ -89,176 +108,194 @@ func Test_NewDistroFromRelease(t *testing.T) { Version: "3.0.20240417", VersionID: "3.0", }, - expectedType: Azure, - expectedRawVersion: "3.0", + expected: &Distro{ + Type: Azure, + Version: "3.0", + }, + major: "3", + minor: "0", }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - d, err := NewFromRelease(test.release) - if test.expectErr { - require.Error(t, err) - return - } else { - require.NoError(t, err) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.expectErr == nil { + tt.expectErr = require.NoError } - assert.Equal(t, test.expectedType, d.Type) - if test.expectedVersion != "" { - assert.Equal(t, test.expectedVersion, d.Version.String()) + distro, err := NewFromRelease(tt.release) + tt.expectErr(t, err) + if err != nil { + return } - if test.expectedRawVersion != "" { - assert.Equal(t, test.expectedRawVersion, d.FullVersion()) + + if d := cmp.Diff(tt.expected, distro, cmpopts.IgnoreUnexported(Distro{})); d != "" { + t.Errorf("unexpected result: %s", d) } + assert.Equal(t, tt.major, distro.MajorVersion(), "unexpected major version") + assert.Equal(t, tt.minor, distro.MinorVersion(), "unexpected minor version") }) } } func Test_NewDistroFromRelease_Coverage(t *testing.T) { + observedDistros := stringutil.NewStringSet() + definedDistros := stringutil.NewStringSet() + + for _, distroType := range All { + definedDistros.Add(string(distroType)) + } + + // Somewhat cheating with Windows. There is no support for detecting/parsing a Windows OS, so it is not + // possible to comply with this test unless it is added manually to the "observed distros" + definedDistros.Remove(string(Windows)) + tests := []struct { - fixture string - Type Type - Version string + Name string + Type Type + Version string + LabelVersion string }{ { - fixture: "test-fixtures/os/alpine", + Name: "test-fixtures/os/alpine", Type: Alpine, Version: "3.11.6", }, { - fixture: "test-fixtures/os/amazon", + Name: "test-fixtures/os/alpine-edge", + Type: Alpine, + Version: "3.22.0_alpha20250108", + }, + { + Name: "test-fixtures/os/amazon", Type: AmazonLinux, - Version: "2.0.0", + Version: "2", }, { - fixture: "test-fixtures/os/busybox", + Name: "test-fixtures/os/busybox", Type: Busybox, Version: "1.31.1", }, { - fixture: "test-fixtures/os/centos", + Name: "test-fixtures/os/centos", Type: CentOS, - Version: "8.0.0", + Version: "8", }, { - fixture: "test-fixtures/os/debian", + Name: "test-fixtures/os/debian", Type: Debian, - Version: "8.0.0", + Version: "8", }, { - fixture: "test-fixtures/os/fedora", + Name: "test-fixtures/os/debian-sid", + Type: Debian, + LabelVersion: "trixie", + }, + { + Name: "test-fixtures/os/fedora", Type: Fedora, - Version: "31.0.0", + Version: "31", }, { - fixture: "test-fixtures/os/redhat", + Name: "test-fixtures/os/redhat", Type: RedHat, - Version: "7.3.0", + Version: "7.3", }, { - fixture: "test-fixtures/os/ubuntu", - Type: Ubuntu, - Version: "20.4.0", + Name: "test-fixtures/os/ubuntu", + Type: Ubuntu, + Version: "20.04", + LabelVersion: "focal", }, { - fixture: "test-fixtures/os/oraclelinux", + Name: "test-fixtures/os/oraclelinux", Type: OracleLinux, - Version: "8.3.0", + Version: "8.3", }, { - fixture: "test-fixtures/os/custom", + Name: "test-fixtures/os/custom", Type: RedHat, - Version: "8.0.0", + Version: "8", }, { - fixture: "test-fixtures/os/opensuse-leap", + Name: "test-fixtures/os/opensuse-leap", Type: OpenSuseLeap, - Version: "15.2.0", + Version: "15.2", }, { - fixture: "test-fixtures/os/sles", + Name: "test-fixtures/os/sles", Type: SLES, - Version: "15.2.0", + Version: "15.2", }, { - fixture: "test-fixtures/os/photon", + Name: "test-fixtures/os/photon", Type: Photon, - Version: "2.0.0", + Version: "2.0", }, { - fixture: "test-fixtures/os/arch", - Type: ArchLinux, + Name: "test-fixtures/os/arch", + Type: ArchLinux, }, { - fixture: "test-fixtures/partial-fields/missing-id", + Name: "test-fixtures/partial-fields/missing-id", Type: Debian, - Version: "8.0.0", + Version: "8", }, { - fixture: "test-fixtures/partial-fields/unknown-id", + Name: "test-fixtures/partial-fields/unknown-id", Type: Debian, - Version: "8.0.0", + Version: "8", }, { - fixture: "test-fixtures/os/centos6", + Name: "test-fixtures/os/centos6", Type: CentOS, - Version: "6.0.0", + Version: "6", }, { - fixture: "test-fixtures/os/centos5", + Name: "test-fixtures/os/centos5", Type: CentOS, - Version: "5.7.0", + Version: "5.7", }, { - fixture: "test-fixtures/os/mariner", + Name: "test-fixtures/os/mariner", Type: Mariner, - Version: "1.0.0", + Version: "1.0", }, { - fixture: "test-fixtures/os/azurelinux", + Name: "test-fixtures/os/azurelinux", Type: Azure, - Version: "3.0.0", + Version: "3.0", }, { - fixture: "test-fixtures/os/rockylinux", + Name: "test-fixtures/os/rockylinux", Type: RockyLinux, - Version: "8.4.0", + Version: "8.4", }, { - fixture: "test-fixtures/os/almalinux", + Name: "test-fixtures/os/almalinux", Type: AlmaLinux, - Version: "8.4.0", + Version: "8.4", }, { - fixture: "test-fixtures/os/gentoo", - Type: Gentoo, + Name: "test-fixtures/os/gentoo", + Type: Gentoo, }, { - fixture: "test-fixtures/os/wolfi", + Name: "test-fixtures/os/wolfi", Type: Wolfi, + Version: "20220914", }, { - fixture: "test-fixtures/os/chainguard", + Name: "test-fixtures/os/chainguard", Type: Chainguard, + Version: "20230214", }, } - observedDistros := stringutil.NewStringSet() - definedDistros := stringutil.NewStringSet() - - for _, distroType := range All { - definedDistros.Add(string(distroType)) - } - - // Somewhat cheating with Windows. There is no support for detecting/parsing a Windows OS, so it is not - // possible to comply with this test unless it is added manually to the "observed distros" - definedDistros.Remove(string(Windows)) - - for _, test := range tests { - t.Run(test.fixture, func(t *testing.T) { - s, err := directorysource.NewFromPath(test.fixture) + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + s, err := directorysource.NewFromPath(tt.Name) require.NoError(t, err) resolver, err := s.FileResolver(source.SquashedScope) @@ -274,10 +311,9 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) { observedDistros.Add(d.Type.String()) - assert.Equal(t, test.Type, d.Type) - if test.Version != "" { - assert.Equal(t, d.Version.String(), test.Version) - } + assert.Equal(t, tt.Type, d.Type, "unexpected distro type") + assert.Equal(t, tt.LabelVersion, d.Codename, "unexpected label version") + assert.Equal(t, tt.Version, d.Version, "unexpected version") }) } @@ -324,7 +360,7 @@ func TestDistro_FullVersion(t *testing.T) { Version: test.version, }) require.NoError(t, err) - assert.Equal(t, test.expected, d.FullVersion()) + assert.Equal(t, test.expected, d.Version) }) } diff --git a/grype/distro/test-fixtures/os/alpine-edge/etc/os-release b/grype/distro/test-fixtures/os/alpine-edge/etc/os-release new file mode 100644 index 00000000000..c7133dc2390 --- /dev/null +++ b/grype/distro/test-fixtures/os/alpine-edge/etc/os-release @@ -0,0 +1,6 @@ +NAME="Alpine Linux" +ID=alpine +VERSION_ID=3.22.0_alpha20250108 +PRETTY_NAME="Alpine Linux edge" +HOME_URL="https://alpinelinux.org/" +BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues" \ No newline at end of file diff --git a/grype/distro/test-fixtures/os/debian-sid/usr/lib/os-release b/grype/distro/test-fixtures/os/debian-sid/usr/lib/os-release new file mode 100644 index 00000000000..c32b48d1edd --- /dev/null +++ b/grype/distro/test-fixtures/os/debian-sid/usr/lib/os-release @@ -0,0 +1,7 @@ +PRETTY_NAME="Debian GNU/Linux trixie/sid" +NAME="Debian GNU/Linux" +VERSION_CODENAME=trixie +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" diff --git a/grype/grypeerr/errors.go b/grype/grypeerr/errors.go index a7a8a246366..f1baf28059a 100644 --- a/grype/grypeerr/errors.go +++ b/grype/grypeerr/errors.go @@ -1,6 +1,10 @@ package grypeerr var ( - // ErrAboveSeverityThreshold indicates when a vulnerability severity is discovered that is above the given --fail-on severity value + // ErrAboveSeverityThreshold indicates when a vulnerability severity is discovered that is equal + // or above the given --fail-on severity value. ErrAboveSeverityThreshold = NewExpectedErr("discovered vulnerabilities at or above the severity threshold") + + // ErrDBUpgradeAvailable indicates that a DB upgrade is available. + ErrDBUpgradeAvailable = NewExpectedErr("db upgrade available") ) diff --git a/grype/internal/packagemetadata/discover_type_names.go b/grype/internal/packagemetadata/discover_type_names.go index 617e18d8585..a59748758c3 100644 --- a/grype/internal/packagemetadata/discover_type_names.go +++ b/grype/internal/packagemetadata/discover_type_names.go @@ -16,7 +16,7 @@ import ( var metadataExceptions = strset.New( "FileMetadata", - "PURLFileMetadata", + "SBOMFileMetadata", "PURLLiteralMetadata", "CPELiteralMetadata", ) diff --git a/grype/load_vulnerability_db.go b/grype/load_vulnerability_db.go index ba303435b16..286fcdb1e4b 100644 --- a/grype/load_vulnerability_db.go +++ b/grype/load_vulnerability_db.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/grype/internal/log" ) -func LoadVulnerabilityDB(distCfg v6dist.Config, installCfg v6inst.Config, update bool) (vulnerability.Provider, *v6.Status, error) { +func LoadVulnerabilityDB(distCfg v6dist.Config, installCfg v6inst.Config, update bool) (vulnerability.Provider, *vulnerability.ProviderStatus, error) { client, err := v6dist.NewClient(distCfg) if err != nil { return nil, nil, fmt.Errorf("unable to create distribution client: %w", err) @@ -36,8 +36,8 @@ func LoadVulnerabilityDB(distCfg v6dist.Config, installCfg v6inst.Config, update } s := c.Status() - if s.Err != nil { - return nil, nil, s.Err + if s.Error != nil { + return nil, nil, s.Error } rdr, err := c.Reader() diff --git a/grype/match/details.go b/grype/match/details.go index aeeabad98b2..5c7ac53fbe3 100644 --- a/grype/match/details.go +++ b/grype/match/details.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/mitchellh/hashstructure/v2" + "github.com/gohugoio/hashstructure" ) type Details []Detail @@ -43,7 +43,7 @@ func (m Details) Types() (tys []Type) { } func (m Detail) ID() string { - f, err := hashstructure.Hash(&m, hashstructure.FormatV2, &hashstructure.HashOptions{ + f, err := hashstructure.Hash(&m, &hashstructure.HashOptions{ ZeroNil: true, SlicesAsSets: true, }) diff --git a/grype/match/fingerprint.go b/grype/match/fingerprint.go index d4950ee65c3..cec830fcd09 100644 --- a/grype/match/fingerprint.go +++ b/grype/match/fingerprint.go @@ -3,7 +3,7 @@ package match import ( "fmt" - "github.com/mitchellh/hashstructure/v2" + "github.com/gohugoio/hashstructure" "github.com/anchore/grype/grype/pkg" ) @@ -24,7 +24,7 @@ func (m Fingerprint) String() string { } func (m Fingerprint) ID() string { - f, err := hashstructure.Hash(&m, hashstructure.FormatV2, &hashstructure.HashOptions{ + f, err := hashstructure.Hash(&m, &hashstructure.HashOptions{ ZeroNil: true, SlicesAsSets: true, }) diff --git a/grype/match/matcher.go b/grype/match/matcher.go index 1e8e387faf6..ceb0e42183d 100644 --- a/grype/match/matcher.go +++ b/grype/match/matcher.go @@ -1,6 +1,9 @@ package match import ( + "errors" + "fmt" + "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" @@ -16,3 +19,26 @@ type Matcher interface { // after all matches are found Match(vp vulnerability.Provider, p pkg.Package) ([]Match, []IgnoredMatch, error) } + +// fatalError can be returned from a Matcher to indicate the matching process should stop. +// When fatalError(s) are encountered by the top-level matching process, these will be returned as errors to the caller. +type fatalError struct { + matcher MatcherType + inner error +} + +// NewFatalError creates a new fatalError wrapping the given error +func NewFatalError(matcher MatcherType, e error) error { + return fatalError{matcher: matcher, inner: e} +} + +// Error implements the error interface for fatalError. +func (f fatalError) Error() string { + return fmt.Sprintf("%s encountered a fatal error: %v", f.matcher, f.inner) +} + +// IsFatalError returns true if err includes a fatalError +func IsFatalError(err error) bool { + var fe fatalError + return err != nil && errors.As(err, &fe) +} diff --git a/grype/match/matches.go b/grype/match/matches.go index 264703920e8..7ce27f6b852 100644 --- a/grype/match/matches.go +++ b/grype/match/matches.go @@ -100,7 +100,8 @@ func (r *Matches) addOrMerge(newMatch Match, newFp Fingerprint) { // case A if err := existingMatch.Merge(newMatch); err != nil { log.WithFields("original", existingMatch.String(), "new", newMatch.String(), "error", err).Warn("unable to merge matches") - // TODO: dropped match in this case, we should figure a way to handle this + // at least capture the additional details + existingMatch.Details = append(existingMatch.Details, newMatch.Details...) } r.byFingerprint[newFp] = existingMatch @@ -125,6 +126,8 @@ func (r *Matches) mergeCoreMatches(newMatch Match, newFp Fingerprint, existingFi // case B1 if replaced := r.replace(newMatch, existingFp, newFp, existingMatch.Details...); !replaced { log.WithFields("original", existingMatch.String(), "new", newMatch.String()).Trace("unable to replace match") + // at least capture the new details + existingMatch.Details = append(existingMatch.Details, newMatch.Details...) } else { return true } @@ -132,7 +135,9 @@ func (r *Matches) mergeCoreMatches(newMatch Match, newFp Fingerprint, existingFi // case B2 if err := existingMatch.Merge(newMatch); err != nil { - log.WithFields("original", existingMatch.String(), "new", newMatch.String(), "error", err).Warn("unable to merge matches") + log.WithFields("original", existingMatch.String(), "new", newMatch.String(), "error", err).Trace("unable to merge matches") + // at least capture the new details + existingMatch.Details = append(existingMatch.Details, newMatch.Details...) } else { return true } diff --git a/grype/matcher/apk/matcher_test.go b/grype/matcher/apk/matcher_test.go index 353c973ddb8..4f068cafd02 100644 --- a/grype/matcher/apk/matcher_test.go +++ b/grype/matcher/apk/matcher_test.go @@ -61,7 +61,7 @@ func TestSecDBOnlyMatch(t *testing.T) { SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": d.Type.String(), - "version": d.RawVersion, + "version": d.Version, }, "package": map[string]string{ "name": "libvncserver", @@ -140,7 +140,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) { SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": d.Type.String(), - "version": d.RawVersion, + "version": d.Version, }, "package": map[string]string{ "name": "libvncserver", @@ -226,7 +226,7 @@ func TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) { SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": d.Type.String(), - "version": d.RawVersion, + "version": d.Version, }, "package": map[string]string{ "name": "libvncserver", @@ -306,7 +306,7 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) { SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": d.Type.String(), - "version": d.RawVersion, + "version": d.Version, }, "package": map[string]string{ "name": "libvncserver", @@ -659,7 +659,7 @@ func TestNVDMatchCanceledByOriginPackageInSecDB(t *testing.T) { vp := mock.VulnerabilityProvider(nvdVuln, secDBVuln) m := Matcher{} - d, err := distro.New(distro.Wolfi, "") + d, err := distro.New(distro.Wolfi, "", "") if err != nil { t.Fatalf("failed to create a new distro: %+v", err) } @@ -734,7 +734,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) { SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": d.Type.String(), - "version": d.RawVersion, + "version": d.Version, }, "package": map[string]string{ "name": "musl", @@ -805,7 +805,7 @@ func TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) { SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": d.Type.String(), - "version": d.RawVersion, + "version": d.Version, }, "package": map[string]string{ "name": "musl", @@ -903,6 +903,7 @@ func assertMatches(t *testing.T, expected, actual []match.Match) { var opts = []cmp.Option{ cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), cmpopts.IgnoreFields(pkg.Package{}, "Locations"), + cmpopts.IgnoreUnexported(distro.Distro{}), } if diff := cmp.Diff(expected, actual, opts...); diff != "" { diff --git a/grype/matcher/internal/cpe.go b/grype/matcher/internal/cpe.go index 00de88f8758..0af04b69561 100644 --- a/grype/matcher/internal/cpe.go +++ b/grype/matcher/internal/cpe.go @@ -74,9 +74,13 @@ func MatchPackageByCPEs(provider vulnerability.Provider, p pkg.Package, upstream searchVersion = transformJvmVersion(searchVersion, c.Attributes.Update) } - verObj, err := version.NewVersion(searchVersion, format) - if err != nil { - return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) + var verObj *version.Version + var err error + if searchVersion != "" { + verObj, err = version.NewVersion(searchVersion, format) + if err != nil { + return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) + } } // find all vulnerability records in the DB for the given CPE (not including version comparisons) @@ -95,7 +99,7 @@ func MatchPackageByCPEs(provider vulnerability.Provider, p pkg.Package, upstream // relative to the current version information from the CPE (or the package) then the given package // is vulnerable. for _, vuln := range vulns { - addNewMatch(matchesByFingerprint, vuln, p, *verObj, upstreamMatcher, c) + addNewMatch(matchesByFingerprint, vuln, p, verObj, upstreamMatcher, c) } } @@ -110,7 +114,7 @@ func transformJvmVersion(searchVersion, updateCpeField string) string { return searchVersion } -func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vulnerability.Vulnerability, p pkg.Package, searchVersion version.Version, upstreamMatcher match.MatcherType, searchedByCPE cpe.CPE) { +func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vulnerability.Vulnerability, p pkg.Package, searchVersion *version.Version, upstreamMatcher match.MatcherType, searchedByCPE cpe.CPE) { candidateMatch := match.Match{ Vulnerability: vuln, @@ -186,7 +190,11 @@ func addMatchDetails(existingDetails []match.Detail, newDetails match.Detail) [] return existingDetails } -func filterCPEsByVersion(pkgVersion version.Version, allCPEs []cpe.CPE) (matchedCPEs []cpe.CPE) { +func filterCPEsByVersion(pkgVersion *version.Version, allCPEs []cpe.CPE) (matchedCPEs []cpe.CPE) { + if pkgVersion == nil { + // all CPEs are valid in the case when a version is not specified + return allCPEs + } for _, c := range allCPEs { if c.Attributes.Version == wfn.Any || c.Attributes.Version == wfn.NA { matchedCPEs = append(matchedCPEs, c) @@ -208,7 +216,7 @@ func filterCPEsByVersion(pkgVersion version.Version, allCPEs []cpe.CPE) (matched continue } - satisfied, err := constraint.Satisfied(&pkgVersion) + satisfied, err := constraint.Satisfied(pkgVersion) if err != nil || satisfied { // if we can't check for version satisfaction, don't filter out the CPE matchedCPEs = append(matchedCPEs, c) diff --git a/grype/matcher/internal/cpe_test.go b/grype/matcher/internal/cpe_test.go index 0a2a2155c9d..e61d44de7dc 100644 --- a/grype/matcher/internal/cpe_test.go +++ b/grype/matcher/internal/cpe_test.go @@ -216,17 +216,147 @@ func TestFindMatchesByPackageCPE(t *testing.T) { }, }, { - name: "suppress matching when missing version", + name: "return all possible matches when missing version", p: pkg.Package{ CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:*:activerecord:activerecord:unknown:rando1:*:ra:*:ruby:*:*", ""), - cpe.Must("cpe:2.3:*:activerecord:activerecord:unknown:rando4:*:re:*:rails:*:*", ""), + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, + expected: []match.Match{ + { + + Vulnerability: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ID: "CVE-2017-fake-1"}, + }, + Package: pkg.Package{ + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), + }, + Name: "activerecord", + Version: "", // important! + Language: syftPkg.Ruby, + Type: syftPkg.GemPkg, + }, + + Details: []match.Detail{ + { + Type: match.CPEMatch, + Confidence: 0.9, + SearchedBy: match.CPEParameters{ + CPEs: []string{ + "cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", //important! + }, + Namespace: "nvd:cpe", + Package: match.CPEPackageParameter{ + Name: "activerecord", + Version: "", // important! + }, + }, + Found: match.CPEResult{ + CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, + VersionConstraint: "< 3.7.6 (semver)", + VulnerabilityID: "CVE-2017-fake-1", + }, + Matcher: matcher, + }, + }, + }, + { + + Vulnerability: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ID: "CVE-2017-fake-2"}, + }, + Package: pkg.Package{ + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), + }, + Name: "activerecord", + Version: "", // important! + Language: syftPkg.Ruby, + Type: syftPkg.GemPkg, + }, + + Details: []match.Detail{ + { + Type: match.CPEMatch, + Confidence: 0.9, + SearchedBy: match.CPEParameters{ + CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*"}, //important! + Namespace: "nvd:cpe", + Package: match.CPEPackageParameter{ + Name: "activerecord", + Version: "", // important! + }, + }, + Found: match.CPEResult{ + CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"}, + VersionConstraint: "< 3.7.4 (semver)", + VulnerabilityID: "CVE-2017-fake-2", + }, + Matcher: matcher, + }, + }, + }, + { + + Vulnerability: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ID: "CVE-2017-fake-3"}, + }, + Package: pkg.Package{ + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), + }, + Name: "activerecord", + Version: "", // important! + Language: syftPkg.Ruby, + Type: syftPkg.GemPkg, + }, + Details: []match.Detail{ + { + Type: match.CPEMatch, + Confidence: 0.9, + SearchedBy: match.CPEParameters{ + CPEs: []string{ + "cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", //important! + "cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", //important! + }, + Namespace: "nvd:cpe", + Package: match.CPEPackageParameter{ + Name: "activerecord", + Version: "", // important! + }, + }, + Found: match.CPEResult{ + CPEs: []string{"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*"}, + VersionConstraint: "= 4.0.1 (semver)", + VulnerabilityID: "CVE-2017-fake-3", + }, + Matcher: matcher, + }, + }, + }, + }, + }, + { + name: "suppress matching when version is unknown", + p: pkg.Package{ + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), + cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), + }, + Name: "activerecord", + Version: "unknown", + Language: syftPkg.Ruby, + Type: syftPkg.GemPkg, + }, expected: []match.Match{}, }, { @@ -908,6 +1038,20 @@ func TestFilterCPEsByVersion(t *testing.T) { "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, + { + name: "do not filter on empty version", + version: "", // important! + vulnerabilityCPEs: []string{ + "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", + "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", + "cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*", + }, + expected: []string{ + "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", + "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", + "cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*", + }, + }, } for _, test := range tests { @@ -918,13 +1062,15 @@ func TestFilterCPEsByVersion(t *testing.T) { vulnerabilityCPEs[idx] = cpe.Must(c, "") } - versionObj, err := version.NewVersion(test.version, version.UnknownFormat) - if err != nil { - t.Fatalf("unable to get version: %+v", err) + var versionObj *version.Version + var err error + if test.version != "" { + versionObj, err = version.NewVersion(test.version, version.UnknownFormat) + require.NoError(t, err) } // run the test subject... - actual := filterCPEsByVersion(*versionObj, vulnerabilityCPEs) + actual := filterCPEsByVersion(versionObj, vulnerabilityCPEs) // format CPE objects to string... actualStrs := make([]string, len(actual)) diff --git a/grype/matcher/internal/distro.go b/grype/matcher/internal/distro.go index 1608522f9e8..65179bc66ba 100644 --- a/grype/matcher/internal/distro.go +++ b/grype/matcher/internal/distro.go @@ -23,13 +23,18 @@ func MatchPackageByDistro(provider vulnerability.Provider, p pkg.Package, upstre return nil, nil, nil } - verObj, err := version.NewVersionFromPkg(p) - if err != nil { - if errors.Is(err, version.ErrUnsupportedVersion) { - log.WithFields("error", err).Tracef("skipping package '%s@%s'", p.Name, p.Version) - return nil, nil, nil + var verObj *version.Version + var err error + + if p.Version != "" { + verObj, err = version.NewVersionFromPkg(p) + if err != nil { + if errors.Is(err, version.ErrUnsupportedVersion) { + log.WithFields("error", err).Tracef("skipping package '%s@%s'", p.Name, p.Version) + return nil, nil, nil + } + return nil, nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) } - return nil, nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) } var matches []match.Match @@ -54,7 +59,7 @@ func MatchPackageByDistro(provider vulnerability.Provider, p pkg.Package, upstre SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": p.Distro.Type.String(), - "version": p.Distro.RawVersion, + "version": p.Distro.Version, }, // why include the package information? The given package searched with may be a source package // for another package that is installed on the system. This makes it apparent exactly what @@ -78,5 +83,5 @@ func MatchPackageByDistro(provider vulnerability.Provider, p pkg.Package, upstre } func isUnknownVersion(v string) bool { - return v == "" || strings.ToLower(v) == "unknown" + return strings.ToLower(v) == "unknown" } diff --git a/grype/matcher/internal/only_vulnerable_versions.go b/grype/matcher/internal/only_vulnerable_versions.go index 81b2315cdfd..bd480411aaa 100644 --- a/grype/matcher/internal/only_vulnerable_versions.go +++ b/grype/matcher/internal/only_vulnerable_versions.go @@ -8,7 +8,7 @@ import ( // onlyVulnerableVersion returns a criteria object that tests affected vulnerability ranges against the provided version func onlyVulnerableVersions(v *version.Version) vulnerability.Criteria { - if v == nil { + if v == nil || v.Raw == "" { // if no version is provided, match everything return search.ByFunc(func(_ vulnerability.Vulnerability) (bool, string, error) { return true, "", nil diff --git a/grype/matcher/java/matcher.go b/grype/matcher/java/matcher.go index 23b5a1a5d81..218200aca8f 100644 --- a/grype/matcher/java/matcher.go +++ b/grype/matcher/java/matcher.go @@ -59,8 +59,7 @@ func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Ma if strings.Contains(err.Error(), "no artifact found") { log.Debugf("no upstream maven artifact found for %s", p.Name) } else { - log.WithFields("package", p.Name, "error", err).Error("failed to resolve package details with maven") - return nil, nil, fmt.Errorf("resolving package details with maven: %w", err) + return nil, nil, match.NewFatalError(match.JavaMatcher, fmt.Errorf("resolving details for package %q with maven: %w", p.Name, err)) } } else { matches = append(matches, upstreamMatches...) @@ -82,23 +81,53 @@ func (m *Matcher) matchUpstreamMavenPackages(store vulnerability.Provider, p pkg ctx := context.Background() - if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok { - for _, digest := range metadata.ArchiveDigests { - if digest.Algorithm == "sha1" { - indirectPackage, err := m.GetMavenPackageBySha(ctx, digest.Value) - if err != nil { - return nil, err - } - indirectMatches, _, err := internal.MatchPackageByLanguage(store, *indirectPackage, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, indirectMatches...) + // Check if we need to search Maven by SHA + searchMaven, digests := m.shouldSearchMavenBySha(p) + if searchMaven { + // If the artifact and group ID exist are missing, attempt Maven lookup using SHA-1 + for _, digest := range digests { + log.Debugf("searching maven, POM data missing for %s", p.Name) + indirectPackage, err := m.GetMavenPackageBySha(ctx, digest) + if err != nil { + return nil, err + } + indirectMatches, _, err := internal.MatchPackageByLanguage(store, *indirectPackage, m.Type()) + if err != nil { + return nil, err } + matches = append(matches, indirectMatches...) } + } else { + log.Debugf("skipping maven search, POM data present for %s", p.Name) + indirectMatches, _, err := internal.MatchPackageByLanguage(store, p, m.Type()) + if err != nil { + return nil, err + } + matches = append(matches, indirectMatches...) } match.ConvertToIndirectMatches(matches, p) return matches, nil } + +func (m *Matcher) shouldSearchMavenBySha(p pkg.Package) (bool, []string) { + digests := []string{} + + if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok { + // if either the PomArtifactID or PomGroupID is missing, we need to search Maven + if metadata.PomArtifactID == "" || metadata.PomGroupID == "" { + for _, digest := range metadata.ArchiveDigests { + if digest.Algorithm == "sha1" && digest.Value != "" { + digests = append(digests, digest.Value) + } + } + // if we need to search Maven but no valid SHA-1 digests exist, skip search + if len(digests) == 0 { + return false, digests + } + } + } + + return len(digests) > 0, digests +} diff --git a/grype/matcher/java/matcher_mocks_test.go b/grype/matcher/java/matcher_mocks_test.go index f9b703e0a10..c781c7618ae 100644 --- a/grype/matcher/java/matcher_mocks_test.go +++ b/grype/matcher/java/matcher_mocks_test.go @@ -23,6 +23,18 @@ func newMockProvider() vulnerability.Provider { Constraint: version.MustGetConstraint(">=5.0.1,<5.1.7", version.UnknownFormat), Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: "github:language:" + syftPkg.Java.String()}, }, + // Package name is expected to resolve to : if pom groupID and artifactID is present + // See JavaResolver.Names: https://github.com/anchore/grype/blob/402067e958a4fa9d20384752351d6c54b0436ba1/grype/db/v6/name/java.go#L19 + { + PackageName: "org.springframework:spring-webmvc", + Constraint: version.MustGetConstraint(">=5.0.0,<5.1.7", version.UnknownFormat), + Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "github:language:" + syftPkg.Java.String()}, + }, + { + PackageName: "org.springframework:spring-webmvc", + Constraint: version.MustGetConstraint(">=5.0.1,<5.1.7", version.UnknownFormat), + Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: "github:language:" + syftPkg.Java.String()}, + }, // unexpected... { PackageName: "org.springframework.spring-webmvc", diff --git a/grype/matcher/java/matcher_test.go b/grype/matcher/java/matcher_test.go index ad9531de45c..83e2a4b4eb3 100644 --- a/grype/matcher/java/matcher_test.go +++ b/grype/matcher/java/matcher_test.go @@ -26,57 +26,266 @@ func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) { } store := newMockProvider() - p := pkg.Package{ - ID: pkg.ID(uuid.NewString()), - Name: "org.springframework.spring-webmvc", - Version: "5.1.5.RELEASE", - Language: syftPkg.Java, - Type: syftPkg.JavaPkg, - Metadata: pkg.JavaMetadata{ - ArchiveDigests: []pkg.Digest{ + // Define test cases + testCases := []struct { + testname string + testExpectRateLimit bool + packages []pkg.Package + }{ + { + testname: "do not search maven - metadata present", + testExpectRateLimit: false, + packages: []pkg.Package{ { - Algorithm: "sha1", - Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", + ID: pkg.ID(uuid.NewString()), + Name: "org.springframework.spring-webmvc", + Version: "5.1.5.RELEASE", + Language: syftPkg.Java, + Type: syftPkg.JavaPkg, + Metadata: pkg.JavaMetadata{ + PomArtifactID: "spring-webmvc", + PomGroupID: "org.springframework", + ArchiveDigests: []pkg.Digest{ + { + Algorithm: "sha1", + Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", + }, + }, + }, + }, + }, + }, + { + testname: "search maven - missing metadata", + testExpectRateLimit: false, + packages: []pkg.Package{ + { + ID: pkg.ID(uuid.NewString()), + Name: "org.springframework.spring-webmvc", + Version: "5.1.5.RELEASE", + Language: syftPkg.Java, + Type: syftPkg.JavaPkg, + Metadata: pkg.JavaMetadata{ + PomArtifactID: "", + PomGroupID: "", + ArchiveDigests: []pkg.Digest{ + { + Algorithm: "sha1", + Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", + }, + }, + }, + }, + }, + }, + { + testname: "search maven - missing sha1 error", + testExpectRateLimit: false, + packages: []pkg.Package{ + { + ID: pkg.ID(uuid.NewString()), + Name: "org.springframework.spring-webmvc", + Version: "5.1.5.RELEASE", + Language: syftPkg.Java, + Type: syftPkg.JavaPkg, + Metadata: pkg.JavaMetadata{ + PomArtifactID: "", + PomGroupID: "", + ArchiveDigests: []pkg.Digest{ + { + Algorithm: "sha1", + Value: "", + }, + }, + }, }, }, }, } t.Run("matching from maven search results", func(t *testing.T) { - matcher := newMatcher(mockMavenSearcher{ - pkg: p, - }) - actual, _ := matcher.matchUpstreamMavenPackages(store, p) - - assert.Len(t, actual, 2, "unexpected matches count") - - foundCVEs := stringutil.NewStringSet() - for _, v := range actual { - foundCVEs.Add(v.Vulnerability.ID) - - require.NotEmpty(t, v.Details) - for _, d := range v.Details { - assert.Equal(t, match.ExactIndirectMatch, d.Type, "indirect match not indicated") - assert.Equal(t, matcher.Type(), d.Matcher, "failed to capture matcher type") - } - assert.Equal(t, p.Name, v.Package.Name, "failed to capture original package name") - } + for _, p := range testCases { + // Adding test isolation + t.Run(p.testname, func(t *testing.T) { + matcher := newMatcher(mockMavenSearcher{ + pkg: p.packages[0], + }) + actual, _ := matcher.matchUpstreamMavenPackages(store, p.packages[0]) - for _, id := range []string{"CVE-2014-fake-2", "CVE-2013-fake-3"} { - if !foundCVEs.Contains(id) { - t.Errorf("missing discovered CVE: %s", id) - } - } - if t.Failed() { - t.Logf("discovered CVES: %+v", foundCVEs) + assert.Len(t, actual, 2, "unexpected matches count") + + foundCVEs := stringutil.NewStringSet() + for _, v := range actual { + foundCVEs.Add(v.Vulnerability.ID) + + require.NotEmpty(t, v.Details) + for _, d := range v.Details { + assert.Equal(t, match.ExactIndirectMatch, d.Type, "indirect match not indicated") + assert.Equal(t, matcher.Type(), d.Matcher, "failed to capture matcher type") + } + assert.Equal(t, p.packages[0].Name, v.Package.Name, "failed to capture original package name") + } + + for _, id := range []string{"CVE-2014-fake-2", "CVE-2013-fake-3"} { + if !foundCVEs.Contains(id) { + t.Errorf("missing discovered CVE: %s", id) + } + } + if t.Failed() { + t.Logf("discovered CVES: %+v", foundCVEs) + } + + }) } }) t.Run("handles maven rate limiting", func(t *testing.T) { - matcher := newMatcher(mockMavenSearcher{simulateRateLimiting: true}) + for _, p := range testCases { + // Adding test isolation + t.Run(p.testname, func(t *testing.T) { + matcher := newMatcher(mockMavenSearcher{simulateRateLimiting: true}) - _, err := matcher.matchUpstreamMavenPackages(store, p) + _, err := matcher.matchUpstreamMavenPackages(store, p.packages[0]) - assert.Errorf(t, err, "should have gotten an error from the rate limiting") + if p.testExpectRateLimit { + assert.Errorf(t, err, "should have gotten an error from the rate limiting") + } + }) + } + }) +} + +func TestMatcherJava_shouldSearchMavenBySha(t *testing.T) { + newMatcher := func(searcher MavenSearcher) *Matcher { + return &Matcher{ + cfg: MatcherConfig{ + ExternalSearchConfig: ExternalSearchConfig{ + SearchMavenUpstream: true, + }, + }, + MavenSearcher: searcher, + } + } + + // Define test cases + testCases := []struct { + testname string + expectedShouldSearchMaven bool + testExpectedError bool + packages []pkg.Package + }{ + { + testname: "do not search maven - metadata present", + expectedShouldSearchMaven: false, + testExpectedError: false, + packages: []pkg.Package{ + { + ID: pkg.ID(uuid.NewString()), + Name: "org.springframework.spring-webmvc", + Version: "5.1.5.RELEASE", + Language: syftPkg.Java, + Type: syftPkg.JavaPkg, + Metadata: pkg.JavaMetadata{ + PomArtifactID: "spring-webmvc", + PomGroupID: "org.springframework", + ArchiveDigests: []pkg.Digest{ + { + Algorithm: "sha1", + Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", + }, + }, + }, + }, + }, + }, + { + testname: "search maven - missing metadata", + expectedShouldSearchMaven: true, + testExpectedError: false, + packages: []pkg.Package{ + { + ID: pkg.ID(uuid.NewString()), + Name: "org.springframework.spring-webmvc", + Version: "5.1.5.RELEASE", + Language: syftPkg.Java, + Type: syftPkg.JavaPkg, + Metadata: pkg.JavaMetadata{ + PomArtifactID: "", + PomGroupID: "", + ArchiveDigests: []pkg.Digest{ + { + Algorithm: "sha1", + Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", + }, + }, + }, + }, + }, + }, + { + testname: "search maven - missing artifactId", + expectedShouldSearchMaven: true, + packages: []pkg.Package{ + { + ID: pkg.ID(uuid.NewString()), + Name: "org.springframework.spring-webmvc", + Version: "5.1.5.RELEASE", + Language: syftPkg.Java, + Type: syftPkg.JavaPkg, + Metadata: pkg.JavaMetadata{ + PomArtifactID: "", + PomGroupID: "org.springframework", + ArchiveDigests: []pkg.Digest{ + { + Algorithm: "sha1", + Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", + }, + }, + }, + }, + }, + }, + { + testname: "do not search maven - missing sha1", + expectedShouldSearchMaven: false, + packages: []pkg.Package{ + { + ID: pkg.ID(uuid.NewString()), + Name: "org.springframework.spring-webmvc", + Version: "5.1.5.RELEASE", + Language: syftPkg.Java, + Type: syftPkg.JavaPkg, + Metadata: pkg.JavaMetadata{ + PomArtifactID: "", + PomGroupID: "", + ArchiveDigests: []pkg.Digest{ + { + Algorithm: "sha1", + Value: "", + }, + }, + }, + }, + }, + }, + } + + t.Run("matching from Maven search results", func(t *testing.T) { + for _, p := range testCases { + // Adding test isolation + t.Run(p.testname, func(t *testing.T) { + matcher := newMatcher(mockMavenSearcher{ + pkg: p.packages[0], + }) + actual, digests := matcher.shouldSearchMavenBySha(p.packages[0]) + + assert.Equal(t, p.expectedShouldSearchMaven, actual, "unexpected decision to search Maven") + + if actual { + assert.NotEmpty(t, digests, "sha digests should not be empty when search is expected") + } + + }) + } }) } diff --git a/grype/matcher/mock/matcher.go b/grype/matcher/mock/matcher.go new file mode 100644 index 00000000000..11e444f0510 --- /dev/null +++ b/grype/matcher/mock/matcher.go @@ -0,0 +1,45 @@ +package mock + +import ( + "errors" + + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +// MatchFunc is a function that takes a vulnerability provider and a package, +// and returns matches, ignored matches, and an error. +type MatchFunc func(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) + +// Matcher is a mock implementation of the match.Matcher interface. This is +// intended for testing purposes only. +type Matcher struct { + typ syftPkg.Type + matchFunc MatchFunc +} + +// New creates a new mock Matcher with the given type and match function. +func New(typ syftPkg.Type, matchFunc MatchFunc) *Matcher { + return &Matcher{ + typ: typ, + matchFunc: matchFunc, + } +} + +func (m Matcher) PackageTypes() []syftPkg.Type { + return []syftPkg.Type{m.typ} +} + +func (m Matcher) Type() match.MatcherType { + return "MOCK" +} + +func (m Matcher) Match(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + if m.matchFunc != nil { + return m.matchFunc(vp, p) + } + + return nil, nil, errors.New("no match function provided") +} diff --git a/grype/matcher/msrc/matcher_test.go b/grype/matcher/msrc/matcher_test.go index 0696dc807b2..f42e5a0a1d2 100644 --- a/grype/matcher/msrc/matcher_test.go +++ b/grype/matcher/msrc/matcher_test.go @@ -21,7 +21,7 @@ func TestMatches(t *testing.T) { // TODO: it would be ideal to test against something that constructs the namespace based on grype-db // and not break the adaption of grype-db - msrcNamespace := fmt.Sprintf("msrc:distro:windows:%s", d.RawVersion) + msrcNamespace := fmt.Sprintf("msrc:distro:windows:%s", d.Version) vp := mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { @@ -29,7 +29,7 @@ func TestMatches(t *testing.T) { ID: "CVE-2016-3333", Namespace: msrcNamespace, }, - PackageName: d.RawVersion, + PackageName: d.Version, Constraint: version.MustGetConstraint("3200970 || 878787 || base", version.KBFormat), }, { @@ -38,7 +38,7 @@ func TestMatches(t *testing.T) { ID: "CVE-2020-made-up", Namespace: msrcNamespace, }, - PackageName: d.RawVersion, + PackageName: d.Version, Constraint: version.MustGetConstraint("778786 || 878787 || base", version.KBFormat), }, // Does not match the product ID @@ -61,7 +61,7 @@ func TestMatches(t *testing.T) { name: "direct KB match", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), - Name: d.RawVersion, + Name: d.Version, Version: "3200970", Type: syftPkg.KbPkg, Distro: d, @@ -74,7 +74,7 @@ func TestMatches(t *testing.T) { name: "multiple direct KB match", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), - Name: d.RawVersion, + Name: d.Version, Version: "878787", Type: syftPkg.KbPkg, Distro: d, @@ -88,7 +88,7 @@ func TestMatches(t *testing.T) { name: "no KBs found", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), - Name: d.RawVersion, + Name: d.Version, // this is the assumed version if no KBs are found Version: "base", Type: syftPkg.KbPkg, diff --git a/grype/pkg/context.go b/grype/pkg/context.go index 5f46a6f9f9c..0279e24a401 100644 --- a/grype/pkg/context.go +++ b/grype/pkg/context.go @@ -1,11 +1,11 @@ package pkg import ( - "github.com/anchore/syft/syft/linux" + "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/source" ) type Context struct { Source *source.Description - Distro *linux.Release + Distro *distro.Distro } diff --git a/grype/pkg/package.go b/grype/pkg/package.go index 5d3a0594f44..abf6b244d30 100644 --- a/grype/pkg/package.go +++ b/grype/pkg/package.go @@ -3,15 +3,16 @@ package pkg import ( "fmt" "regexp" + "slices" "strings" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" cpes "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" ) @@ -44,7 +45,7 @@ type Package struct { Metadata interface{} // This is NOT 1-for-1 the syft metadata! Only the select data needed for vulnerability matching } -func New(p syftPkg.Package) Package { +func New(p syftPkg.Package, enhancers ...Enhancer) Package { metadata, upstreams := dataFromPkg(p) licenseObjs := p.Licenses.ToSlice() @@ -57,7 +58,7 @@ func New(p syftPkg.Package) Package { licenses = []string{} } - return Package{ + out := Package{ ID: ID(p.ID()), Name: p.Name, Version: p.Version, @@ -70,13 +71,25 @@ func New(p syftPkg.Package) Package { Upstreams: upstreams, Metadata: metadata, } + + if len(enhancers) > 0 { + purl, err := packageurl.FromString(p.PURL) + if err != nil { + log.WithFields("purl", purl, "error", err).Debug("unable to parse PURL") + } + for _, e := range enhancers { + e(&out, purl, p) + } + } + + return out } -func FromCollection(catalog *syftPkg.Collection, config SynthesisConfig) []Package { - return FromPackages(catalog.Sorted(), config) +func FromCollection(catalog *syftPkg.Collection, config SynthesisConfig, enhancers ...Enhancer) []Package { + return FromPackages(catalog.Sorted(), config, enhancers...) } -func FromPackages(syftpkgs []syftPkg.Package, config SynthesisConfig) []Package { +func FromPackages(syftpkgs []syftPkg.Package, config SynthesisConfig, enhancers ...Enhancer) []Package { var pkgs []Package for _, p := range syftpkgs { if len(p.CPEs) == 0 { @@ -87,7 +100,7 @@ func FromPackages(syftpkgs []syftPkg.Package, config SynthesisConfig) []Package log.Debugf("no CPEs for package: %s", p) } } - pkgs = append(pkgs, New(p)) + pkgs = append(pkgs, New(p, enhancers...)) } return pkgs @@ -98,7 +111,7 @@ func (p Package) String() string { return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s, upstreams=%d)", p.Type, p.Name, p.Version, len(p.Upstreams)) } -func removePackagesByOverlap(catalog *syftPkg.Collection, relationships []artifact.Relationship, distro *linux.Release) *syftPkg.Collection { +func removePackagesByOverlap(catalog *syftPkg.Collection, relationships []artifact.Relationship, distro *distro.Distro) *syftPkg.Collection { byOverlap := map[artifact.ID]artifact.Relationship{} for _, r := range relationships { if r.Type == artifact.OwnershipByFileOverlapRelationship { @@ -127,8 +140,8 @@ func excludePackage(comprehensiveDistroFeed bool, p syftPkg.Package, parent syft // python 3.9.2 binary // python3.9 3.9.2-1 deb - // If the version is not effectively the same, keep both - if !strings.HasPrefix(parent.Version, p.Version) { + // If the version is not approximately the same, keep both + if !strings.HasPrefix(parent.Version, p.Version) && !strings.HasPrefix(p.Version, parent.Version) { return false } @@ -151,23 +164,23 @@ func excludePackage(comprehensiveDistroFeed bool, p syftPkg.Package, parent syft // distroFeedIsComprehensive returns true if the distro feed // is comprehensive enough that we can drop packages owned by distro packages // before matching. -func distroFeedIsComprehensive(distro *linux.Release) bool { +func distroFeedIsComprehensive(dst *distro.Distro) bool { // TODO: this mechanism should be re-examined once https://github.com/anchore/grype/issues/1426 // is addressed - if distro == nil { + if dst == nil { return false } - if distro.ID == "amzn" { + if dst.Type == distro.AmazonLinux { // AmazonLinux shows "like rhel" but is not an rhel clone // and does not have an exhaustive vulnerability feed. return false } for _, d := range comprehensiveDistros { - if strings.EqualFold(d, distro.ID) { + if strings.EqualFold(string(d), dst.Name()) { return true } - for _, n := range distro.IDLike { - if strings.EqualFold(d, n) { + for _, n := range dst.IDLike { + if strings.EqualFold(string(d), n) { return true } } @@ -177,13 +190,13 @@ func distroFeedIsComprehensive(distro *linux.Release) bool { // computed by: // sqlite3 vulnerability.db 'select distinct namespace from vulnerability where fix_state in ("wont-fix", "not-fixed") order by namespace;' | cut -d ':' -f 1 | sort | uniq -// then removing 'github' and replacing 'redhat' with 'rhel' -var comprehensiveDistros = []string{ - "azurelinux", - "debian", - "mariner", - "rhel", - "ubuntu", +// then removing 'github' +var comprehensiveDistros = []distro.Type{ + distro.Azure, + distro.Debian, + distro.Mariner, + distro.RedHat, + distro.Ubuntu, } func isOSPackage(p syftPkg.Package) bool { @@ -195,7 +208,7 @@ func isOSPackage(p syftPkg.Package) bool { } } -func dataFromPkg(p syftPkg.Package) (interface{}, []UpstreamPackage) { +func dataFromPkg(p syftPkg.Package) (any, []UpstreamPackage) { var metadata interface{} var upstreams []UpstreamPackage @@ -204,6 +217,8 @@ func dataFromPkg(p syftPkg.Package) (interface{}, []UpstreamPackage) { metadata = golangMetadataFromPkg(p) case syftPkg.DpkgDBEntry: upstreams = dpkgDataFromPkg(p) + case syftPkg.DpkgArchiveEntry: + upstreams = dpkgDataFromPkg(p) case syftPkg.RpmArchive, syftPkg.RpmDBEntry: m, u := rpmDataFromPkg(p) upstreams = u @@ -220,6 +235,7 @@ func dataFromPkg(p syftPkg.Package) (interface{}, []UpstreamPackage) { case syftPkg.JavaVMInstallation: metadata = javaVMDataFromPkg(p) } + return metadata, upstreams } @@ -277,16 +293,25 @@ func golangMetadataFromPkg(p syftPkg.Package) interface{} { } func dpkgDataFromPkg(p syftPkg.Package) (upstreams []UpstreamPackage) { - if value, ok := p.Metadata.(syftPkg.DpkgDBEntry); ok { + switch value := p.Metadata.(type) { + case syftPkg.DpkgDBEntry: if value.Source != "" { upstreams = append(upstreams, UpstreamPackage{ Name: value.Source, Version: value.SourceVersion, }) } - } else { + case syftPkg.DpkgArchiveEntry: + if value.Source != "" { + upstreams = append(upstreams, UpstreamPackage{ + Name: value.Source, + Version: value.SourceVersion, + }) + } + default: log.Warnf("unable to extract DPKG metadata for %s", p) } + return upstreams } @@ -398,3 +423,93 @@ func ByID(id ID, pkgs []Package) *Package { } return nil } + +func parseUpstream(pkgName string, value string, pkgType syftPkg.Type) []UpstreamPackage { + if pkgType == syftPkg.RpmPkg { + return handleSourceRPM(pkgName, value) + } + return handleDefaultUpstream(pkgName, value) +} + +func handleDefaultUpstream(pkgName string, value string) []UpstreamPackage { + fields := strings.Split(value, "@") + switch len(fields) { + case 2: + if fields[0] == pkgName { + return nil + } + return []UpstreamPackage{ + { + Name: fields[0], + Version: fields[1], + }, + } + case 1: + if fields[0] == pkgName { + return nil + } + return []UpstreamPackage{ + { + Name: fields[0], + }, + } + } + return nil +} + +func setUpstreamsFromPURL(out *Package, purl packageurl.PackageURL, syftPkg syftPkg.Package) { + if len(out.Upstreams) == 0 { + out.Upstreams = upstreamsFromPURL(purl, syftPkg.Type) + } +} + +// upstreamsFromPURL reads any additional data Grype can use, which is ignored by Syft's PURL conversion +func upstreamsFromPURL(purl packageurl.PackageURL, pkgType syftPkg.Type) (upstreams []UpstreamPackage) { + for _, qualifier := range purl.Qualifiers { + if qualifier.Key == syftPkg.PURLQualifierUpstream { + for _, newUpstream := range parseUpstream(purl.Name, qualifier.Value, pkgType) { + if slices.Contains(upstreams, newUpstream) { + continue + } + upstreams = append(upstreams, newUpstream) + } + } + } + return upstreams +} + +func setDistroFromPURL(out *Package, purl packageurl.PackageURL, _ syftPkg.Package) { + if out.Distro == nil { + out.Distro = distroFromPURL(purl) + } +} + +// distroFromPURL reads distro data for Grype can use, which is ignored by Syft's PURL conversion +func distroFromPURL(purl packageurl.PackageURL) (d *distro.Distro) { + var distroName, distroVersion string + + for _, qualifier := range purl.Qualifiers { + if qualifier.Key == syftPkg.PURLQualifierDistro { + fields := strings.SplitN(qualifier.Value, "-", 2) + distroName = fields[0] + if len(fields) > 1 { + distroVersion = fields[1] + } + } + } + + if distroName != "" { + var err error + d, err = distro.NewFromNameVersion(distroName, distroVersion) + if err != nil { + log.WithFields("purl", purl, "error", err).Debug("unable to create distro from a release") + d = nil + } + } + + return d +} + +type Enhancer func(out *Package, purl packageurl.PackageURL, pkg syftPkg.Package) + +var purlEnhancers = []Enhancer{setUpstreamsFromPURL, setDistroFromPURL} diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go index 0c02886b312..37ac0ed022e 100644 --- a/grype/pkg/package_test.go +++ b/grype/pkg/package_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" @@ -68,6 +69,36 @@ func TestNew(t *testing.T) { }, }, }, + { + name: "dpkg archive with source info", + syftPkg: syftPkg.Package{ + Metadata: syftPkg.DpkgArchiveEntry{ + Package: "pkg-info", + Source: "src-info", + Version: "version-info", + SourceVersion: "src-version-info", + Architecture: "arch-info", + Maintainer: "maintainer-info", + InstalledSize: 10, + Files: []syftPkg.DpkgFileRecord{ + { + Path: "path-info", + Digest: &file.Digest{ + Algorithm: "algo-info", + Value: "digest-info", + }, + IsConfigFile: true, + }, + }, + }, + }, + upstreams: []UpstreamPackage{ + { + Name: "src-info", + Version: "src-version-info", + }, + }, + }, { name: "rpm archive with source info", syftPkg: syftPkg.Package{ @@ -290,6 +321,15 @@ func TestNew(t *testing.T) { }, }, }, + { + name: "github-actions-use-statement", + syftPkg: syftPkg.Package{ + Metadata: syftPkg.GitHubActionsUseStatement{ + Value: "a", + Comment: "a", + }, + }, + }, { name: "golang-metadata", syftPkg: syftPkg.Package{ @@ -337,7 +377,7 @@ func TestNew(t *testing.T) { }, }, { - name: "dart-pub-metadata", + name: "dart-publock-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.DartPubspecLockEntry{ Name: "a", @@ -345,6 +385,33 @@ func TestNew(t *testing.T) { }, }, }, + { + name: "dart-pubspec-metadata", + syftPkg: syftPkg.Package{ + Metadata: syftPkg.DartPubspec{ + Homepage: "a", + Repository: "a", + Documentation: "a", + PublishTo: "a", + Environment: &syftPkg.DartPubspecEnvironment{ + SDK: "a", + Flutter: "a", + }, + Platforms: []string{"a"}, + IgnoredAdvisories: []string{"a"}, + }, + }, + }, + { + name: "homebrew-formula-metadata", + syftPkg: syftPkg.Package{ + Metadata: syftPkg.HomebrewFormula{ + Tap: "a", + Homepage: "a", + Description: "a", + }, + }, + }, { name: "dotnet-metadata", syftPkg: syftPkg.Package{ @@ -655,7 +722,7 @@ func TestNew(t *testing.T) { }, }, { - name: "Php-pecl-entry", + name: "php-pecl-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.PhpPeclEntry{ Name: "a", @@ -664,6 +731,15 @@ func TestNew(t *testing.T) { }, }, }, + { + name: "php-pear-entry", + syftPkg: syftPkg.Package{ + Metadata: syftPkg.PhpPearEntry{ + Name: "a", + Version: "a", + }, + }, + }, { name: "lua-rocks-entry", syftPkg: syftPkg.Package{ @@ -935,22 +1011,44 @@ func Test_RemovePackagesByOverlap(t *testing.T) { }, { name: "python bindings for system RPM install", - sbom: withDistro(catalogWithOverlaps( + sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, []string{"rpm:python3-rpm@4.14.3-26.el8 -> python:rpm@4.14.3"}), "rhel"), expectedPackages: []string{"rpm:python3-rpm@4.14.3-26.el8"}, }, { name: "amzn linux doesn't remove packages in this way", - sbom: withDistro(catalogWithOverlaps( + sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, []string{"rpm:python3-rpm@4.14.3-26.el8 -> python:rpm@4.14.3"}), "amzn"), expectedPackages: []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, }, + { + name: "remove overlapping package when parent version is prefix of child version", + sbom: withLinuxRelease(catalogWithOverlaps( + []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5", "linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5.x86_64+rt"}, + []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5 -> linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5.x86_64+rt"}), "rhel"), + expectedPackages: []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5"}, + }, + { + name: "remove overlapping package when child version is prefix of parent version", + sbom: withLinuxRelease(catalogWithOverlaps( + []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt", "linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5"}, + []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt -> linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5"}), "rhel"), + expectedPackages: []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt"}, + }, + { + name: "do not remove overlapping package when versions are not similar", + sbom: withLinuxRelease(catalogWithOverlaps( + []string{"rpm:kernel@5.14.0-503.40.1.el9_5", "linux-kernel:linux-kernel@6.17"}, + []string{"rpm:kernel@5.14.0-503.40.1.el9_5 -> linux-kernel:linux-kernel@6.17"}), "rhel"), + expectedPackages: []string{"rpm:kernel@5.14.0-503.40.1.el9_5", "linux-kernel:linux-kernel@6.17"}, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - catalog := removePackagesByOverlap(test.sbom.Artifacts.Packages, test.sbom.Relationships, test.sbom.Artifacts.LinuxDistribution) + d := distro.FromRelease(test.sbom.Artifacts.LinuxDistribution) + catalog := removePackagesByOverlap(test.sbom.Artifacts.Packages, test.sbom.Relationships, d) pkgs := FromCollection(catalog, SynthesisConfig{}) var pkgNames []string for _, p := range pkgs { @@ -1031,7 +1129,7 @@ func catalogWithOverlaps(packages []string, overlaps []string) *sbom.SBOM { } } -func withDistro(s *sbom.SBOM, id string) *sbom.SBOM { +func withLinuxRelease(s *sbom.SBOM, id string) *sbom.SBOM { s.Artifacts.LinuxDistribution = &linux.Release{ ID: id, } diff --git a/grype/pkg/provider.go b/grype/pkg/provider.go index 165d490ad00..e06af83b23e 100644 --- a/grype/pkg/provider.go +++ b/grype/pkg/provider.go @@ -6,6 +6,7 @@ import ( "github.com/bmatcuk/doublestar/v2" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/sbom" @@ -15,20 +16,17 @@ var errDoesNotProvide = fmt.Errorf("cannot provide packages from the given sourc // Provide a set of packages and context metadata describing where they were sourced from. func Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { - packages, ctx, s, err := syftSBOMProvider(userInput, config) - if !errors.Is(err, errDoesNotProvide) { - if len(config.Exclusions) > 0 { - var exclusionsErr error - packages, exclusionsErr = filterPackageExclusions(packages, config.Exclusions) - if exclusionsErr != nil { - return nil, ctx, s, exclusionsErr - } - } - log.WithFields("input", userInput).Trace("interpreting input as an SBOM document") - return packages, ctx, s, err + packages, ctx, s, err := provide(userInput, config) + if err != nil { + return nil, Context{}, nil, err } + setContextDistro(packages, &ctx) + return packages, ctx, s, nil +} - packages, ctx, s, err = purlProvider(userInput) +// Provide a set of packages and context metadata describing where they were sourced from. +func provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { + packages, ctx, s, err := purlProvider(userInput, config) if !errors.Is(err, errDoesNotProvide) { log.WithFields("input", userInput).Trace("interpreting input as one or more PURLs") return packages, ctx, s, err @@ -40,6 +38,19 @@ func Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom return packages, ctx, s, err } + packages, ctx, s, err = syftSBOMProvider(userInput, config) + if !errors.Is(err, errDoesNotProvide) { + if len(config.Exclusions) > 0 { + var exclusionsErr error + packages, exclusionsErr = filterPackageExclusions(packages, config.Exclusions) + if exclusionsErr != nil { + return nil, ctx, s, exclusionsErr + } + } + log.WithFields("input", userInput).Trace("interpreting input as an SBOM document") + return packages, ctx, s, err + } + log.WithFields("input", userInput).Trace("passing input to syft for interpretation") return syftProvider(userInput, config) } @@ -92,3 +103,29 @@ func locationMatches(location file.Location, exclusion string) (bool, error) { } return matchesRealPath || matchesVirtualPath, nil } + +func setContextDistro(packages []Package, ctx *Context) { + if ctx.Distro != nil { + return + } + var singleDistro *distro.Distro + for _, p := range packages { + if p.Distro == nil { + continue + } + if singleDistro == nil { + singleDistro = p.Distro + continue + } + if singleDistro.Type != p.Distro.Type || + singleDistro.Version != p.Distro.Version || + singleDistro.Codename != p.Distro.Codename { + return + } + } + + // if there is one distro (with one version) represented, use that + if singleDistro != nil { + ctx.Distro = singleDistro + } +} diff --git a/grype/pkg/purl_provider.go b/grype/pkg/purl_provider.go index 692a3b10ba9..740f34960fe 100644 --- a/grype/pkg/purl_provider.go +++ b/grype/pkg/purl_provider.go @@ -1,19 +1,11 @@ package pkg import ( - "bufio" "fmt" "io" - "os" "strings" - "github.com/mitchellh/go-homedir" - "github.com/scylladb/go-set/strset" - - "github.com/anchore/packageurl-go" - "github.com/anchore/syft/syft/cpe" - "github.com/anchore/syft/syft/linux" - "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -21,41 +13,28 @@ import ( const ( purlInputPrefix = "purl:" singlePurlInputPrefix = "pkg:" - cpesQualifierKey = "cpes" ) type PURLLiteralMetadata struct { PURL string } -type PURLFileMetadata struct { - Path string -} - -func purlProvider(userInput string) ([]Package, Context, *sbom.SBOM, error) { +func purlProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { reader, ctx, err := getPurlReader(userInput) if err != nil { return nil, Context{}, nil, err } - return decodePurlsFromReader(reader, ctx) + s, _, _, err := format.Decode(reader) + if s == nil { + return nil, Context{}, nil, fmt.Errorf("unable to decode purl: %w", err) + } + + return FromCollection(s.Artifacts.Packages, config.SynthesisConfig, purlEnhancers...), ctx, s, nil } func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) { - switch { - case strings.HasPrefix(userInput, purlInputPrefix): - path := strings.TrimPrefix(userInput, purlInputPrefix) - ctx.Source = &source.Description{ - Metadata: PURLFileMetadata{ - Path: path, - }, - } - file, err := openPurlFile(path) - if err != nil { - return nil, ctx, err - } - return file, ctx, nil - case strings.HasPrefix(userInput, singlePurlInputPrefix): + if strings.HasPrefix(userInput, singlePurlInputPrefix) { ctx.Source = &source.Description{ Metadata: PURLLiteralMetadata{ PURL: userInput, @@ -65,196 +44,3 @@ func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) { } return nil, ctx, errDoesNotProvide } - -func openPurlFile(path string) (*os.File, error) { - expandedPath, err := homedir.Expand(path) - if err != nil { - return nil, fmt.Errorf("unable to open purls: %w", err) - } - - f, err := os.Open(expandedPath) - if err != nil { - return nil, fmt.Errorf("unable to open file %s: %w", expandedPath, err) - } - - return f, nil -} - -func decodePurlsFromReader(reader io.Reader, ctx Context) ([]Package, Context, *sbom.SBOM, error) { - scanner := bufio.NewScanner(reader) - var packages []Package - var syftPkgs []pkg.Package - - distros := make(map[string]*strset.Set) - for scanner.Scan() { - rawLine := scanner.Text() - p, syftPkg, distroName, distroVersion, err := purlToPackage(rawLine) - if err != nil { - return nil, Context{}, nil, err - } - if distroName != "" { - if _, ok := distros[distroName]; !ok { - distros[distroName] = strset.New() - } - distros[distroName].Add(distroVersion) - } - if p != nil { - packages = append(packages, *p) - } - if syftPkg != nil { - syftPkgs = append(syftPkgs, *syftPkg) - } - } - - if err := scanner.Err(); err != nil { - return nil, Context{}, nil, err - } - - s := &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(syftPkgs...), - }, - } - // Do we have multiple purls - // purl litteral <-- - // purl file <-- FileMetadata - - // if there is one distro (with one version) represented, use that - if len(distros) == 1 { - for name, versions := range distros { - if versions.Size() == 1 { - version := versions.List()[0] - var codename string - // if there are no digits in the version, it is likely a codename - if !strings.ContainsAny(version, "0123456789") { - codename = version - version = "" - } - ctx.Distro = &linux.Release{ - Name: name, - ID: name, - IDLike: []string{name}, - Version: version, - VersionCodename: codename, - } - s.Artifacts.LinuxDistribution = &linux.Release{ - Name: name, - ID: name, - IDLike: []string{name}, - Version: version, - VersionCodename: codename, - } - } - } - } - - return packages, ctx, s, nil -} - -func purlToPackage(rawLine string) (*Package, *pkg.Package, string, string, error) { - purl, err := packageurl.FromString(rawLine) - if err != nil { - return nil, nil, "", "", fmt.Errorf("unable to decode purl %s: %w", rawLine, err) - } - - var cpes []cpe.CPE - var upstreams []UpstreamPackage - var distroName, distroVersion string - epoch := "0" - - pkgType := pkg.TypeByName(purl.Type) - - for _, qualifier := range purl.Qualifiers { - switch qualifier.Key { - case cpesQualifierKey: - rawCpes := strings.Split(qualifier.Value, ",") - for _, rawCpe := range rawCpes { - c, err := cpe.New(rawCpe, "") - if err != nil { - return nil, nil, "", "", fmt.Errorf("unable to decode cpe %s in purl %s: %w", rawCpe, rawLine, err) - } - cpes = append(cpes, c) - } - case pkg.PURLQualifierEpoch: - epoch = qualifier.Value - case pkg.PURLQualifierUpstream: - upstreams = append(upstreams, parseUpstream(purl.Name, qualifier.Value, pkgType)...) - case pkg.PURLQualifierDistro: - name, version := parseDistroQualifier(qualifier.Value) - if name != "" && version != "" { - distroName = name - distroVersion = version - } - } - } - - version := purl.Version - if purl.Type == packageurl.TypeRPM && !strings.HasPrefix(purl.Version, fmt.Sprintf("%s:", epoch)) { - version = fmt.Sprintf("%s:%s", epoch, purl.Version) - } - - syftPkg := pkg.Package{ - Name: purl.Name, - Version: version, - Type: pkgType, - CPEs: cpes, - PURL: purl.String(), - Language: pkg.LanguageByName(purl.Type), - } - - syftPkg.SetID() - return &Package{ - ID: ID(purl.String()), - CPEs: cpes, - Name: purl.Name, - Version: version, - Type: pkgType, - Language: pkg.LanguageByName(purl.Type), - PURL: purl.String(), - Upstreams: upstreams, - }, &syftPkg, distroName, distroVersion, nil -} - -func parseDistroQualifier(value string) (string, string) { - fields := strings.SplitN(value, "-", 2) - switch len(fields) { - case 2: - return fields[0], fields[1] - case 1: - return fields[0], "" - } - return "", "" -} - -func parseUpstream(pkgName string, value string, pkgType pkg.Type) []UpstreamPackage { - if pkgType == pkg.RpmPkg { - return handleSourceRPM(pkgName, value) - } - return handleDefaultUpstream(pkgName, value) -} - -func handleDefaultUpstream(pkgName string, value string) []UpstreamPackage { - fields := strings.Split(value, "@") - switch len(fields) { - case 2: - if fields[0] == pkgName { - return nil - } - return []UpstreamPackage{ - { - Name: fields[0], - Version: fields[1], - }, - } - case 1: - if fields[0] == pkgName { - return nil - } - return []UpstreamPackage{ - { - Name: fields[0], - }, - } - } - return nil -} diff --git a/grype/pkg/purl_provider_test.go b/grype/pkg/purl_provider_test.go index ca909402b8b..a63d55735ba 100644 --- a/grype/pkg/purl_provider_test.go +++ b/grype/pkg/purl_provider_test.go @@ -7,10 +7,8 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" - "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" + "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -20,7 +18,6 @@ func Test_PurlProvider(t *testing.T) { userInput string context Context pkgs []Package - sbom *sbom.SBOM wantErr require.ErrorAssertionFunc }{ { @@ -41,26 +38,15 @@ func Test_PurlProvider(t *testing.T) { PURL: "pkg:apk/curl@7.61.1", }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1", - }), - }, - }, }, { name: "os with codename", userInput: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", context: Context{ - Distro: &linux.Release{ - Name: "debian", - ID: "debian", - IDLike: []string{"debian"}, - VersionCodename: "jessie", // important! + Distro: &distro.Distro{ + Type: "debian", + IDLike: []string{"debian"}, + Codename: "jessie", // important! }, Source: &source.Description{ Metadata: PURLLiteralMetadata{ @@ -74,6 +60,7 @@ func Test_PurlProvider(t *testing.T) { Version: "2.88dsf-59", Type: pkg.DebPkg, PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", + Distro: &distro.Distro{Type: distro.Debian, Version: "", Codename: "jessie", IDLike: []string{"debian"}}, Upstreams: []UpstreamPackage{ { Name: "sysvinit", @@ -81,22 +68,6 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "sysv-rc", - Version: "2.88dsf-59", - Type: pkg.DebPkg, - PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", - }), - LinuxDistribution: &linux.Release{ - Name: "debian", - ID: "debian", - IDLike: []string{"debian"}, - VersionCodename: "jessie", - }, - }, - }, }, { name: "default upstream", @@ -121,16 +92,6 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "libcrypto3", - Version: "3.3.2", - Type: pkg.ApkPkg, - PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl", - }), - }, - }, }, { name: "upstream with version", @@ -156,25 +117,14 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "libcrypto3", - Version: "3.3.2", - Type: pkg.ApkPkg, - PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", - }), - }, - }, }, { name: "upstream for source RPM", userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", context: Context{ - Distro: &linux.Release{ - Name: "rhel", - ID: "rhel", - IDLike: []string{"rhel"}, + Distro: &distro.Distro{ + Type: "redhat", + IDLike: []string{"redhat"}, Version: "8.10", }, Source: &source.Description{ @@ -186,9 +136,10 @@ func Test_PurlProvider(t *testing.T) { pkgs: []Package{ { Name: "systemd-x", - Version: "0:239-82.el8_10.2", + Version: "239-82.el8_10.2", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", + Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Codename: "", IDLike: []string{"redhat"}}, Upstreams: []UpstreamPackage{ { Name: "systemd", @@ -197,31 +148,14 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "systemd-x", - Version: "0:239-82.el8_10.2", - Type: pkg.RpmPkg, - PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", - }), - LinuxDistribution: &linux.Release{ - Name: "rhel", - ID: "rhel", - IDLike: []string{"rhel"}, - Version: "8.10", - }, - }, - }, }, { name: "RPM with epoch", userInput: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", context: Context{ - Distro: &linux.Release{ - Name: "rhel", - ID: "rhel", - IDLike: []string{"rhel"}, + Distro: &distro.Distro{ + Type: "redhat", + IDLike: []string{"redhat"}, Version: "8.10", }, Source: &source.Description{ @@ -236,6 +170,7 @@ func Test_PurlProvider(t *testing.T) { Version: "1:1.12.8-26.el8", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", + Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Codename: "", IDLike: []string{"redhat"}}, Upstreams: []UpstreamPackage{ { Name: "dbus", @@ -244,103 +179,13 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "dbus-common", - Version: "1:1.12.8-26.el8", - Type: pkg.RpmPkg, - PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", - }), - LinuxDistribution: &linux.Release{ - Name: "rhel", - ID: "rhel", - IDLike: []string{"rhel"}, - Version: "8.10", - }, - }, - }, - }, - { - name: "takes multiple purls", - userInput: "purl:test-fixtures/purl/valid-purl.txt", - context: Context{ - Distro: &linux.Release{ - Name: "debian", - ID: "debian", - IDLike: []string{"debian"}, - Version: "8", - }, - Source: &source.Description{ - Metadata: PURLFileMetadata{ - Path: "test-fixtures/purl/valid-purl.txt", - }, - }, - }, - pkgs: []Package{ - { - Name: "sysv-rc", - Version: "2.88dsf-59", - Type: pkg.DebPkg, - PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit", - Upstreams: []UpstreamPackage{ - { - Name: "sysvinit", - }, - }, - }, - { - Name: "ant", - Version: "1.10.8", - Type: pkg.JavaPkg, - PURL: "pkg:maven/org.apache.ant/ant@1.10.8", - }, - { - Name: "log4j-core", - Version: "2.14.1", - Type: pkg.JavaPkg, - PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", - }, - }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection( - pkg.Package{ - Name: "sysv-rc", - Version: "2.88dsf-59", - Type: pkg.DebPkg, - PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit", - }, - pkg.Package{ - Name: "ant", - Version: "1.10.8", - Type: pkg.JavaPkg, - Language: pkg.Java, - PURL: "pkg:maven/org.apache.ant/ant@1.10.8", - }, - pkg.Package{ - Name: "log4j-core", - Version: "2.14.1", - Type: pkg.JavaPkg, - Language: pkg.Java, - PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", - }), - LinuxDistribution: &linux.Release{ - Name: "debian", - ID: "debian", - IDLike: []string{"debian"}, - Version: "8", - }, - }, - }, }, { name: "infer context when distro is present for single purl", userInput: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", context: Context{ - Distro: &linux.Release{ - Name: "alpine", - ID: "alpine", + Distro: &distro.Distro{ + Type: "alpine", IDLike: []string{"alpine"}, Version: "3.20.3", }, @@ -356,152 +201,66 @@ func Test_PurlProvider(t *testing.T) { Version: "7.61.1", Type: pkg.ApkPkg, PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", - }, - }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", - }), - LinuxDistribution: &linux.Release{ - Name: "alpine", - ID: "alpine", - IDLike: []string{"alpine"}, - Version: "3.20.3", - }, + Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, }, }, }, { - name: "infer context when distro is present for multiple similar purls", - userInput: "purl:test-fixtures/purl/homogeneous-os.txt", + name: "include namespace in name when purl is type Golang", + userInput: "pkg:golang/k8s.io/ingress-nginx@v1.11.2", context: Context{ - Distro: &linux.Release{ - Name: "alpine", - ID: "alpine", - IDLike: []string{"alpine"}, - Version: "3.20.3", - }, Source: &source.Description{ - Metadata: PURLFileMetadata{ - Path: "test-fixtures/purl/homogeneous-os.txt", - }, + Metadata: PURLLiteralMetadata{PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2"}, }, }, pkgs: []Package{ { - Name: "openssl", - Version: "3.2.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", - }, - { - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", - }, - }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "openssl", - Version: "3.2.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", - }, - pkg.Package{ - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", - }), - LinuxDistribution: &linux.Release{ - Name: "alpine", - ID: "alpine", - IDLike: []string{"alpine"}, - Version: "3.20.3", - }, + Name: "k8s.io/ingress-nginx", + Version: "v1.11.2", + Type: pkg.GoModulePkg, + PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2", }, }, }, { - name: "different distro info in purls does not infer context", - userInput: "purl:test-fixtures/purl/different-os.txt", + name: "include complex namespace in name when purl is type Golang", + userInput: "pkg:golang/github.com/wazuh/wazuh@v4.5.0", context: Context{ - // important: no distro info inferred Source: &source.Description{ - Metadata: PURLFileMetadata{ - Path: "test-fixtures/purl/different-os.txt", - }, + Metadata: PURLLiteralMetadata{PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0"}, }, }, pkgs: []Package{ { - Name: "openssl", - Version: "3.2.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", - }, - { - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2", + Name: "github.com/wazuh/wazuh", + Version: "v4.5.0", + Type: pkg.GoModulePkg, + PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0", }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "openssl", - Version: "3.2.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", - }, - pkg.Package{ - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2", - }), - }, - }, - }, - { - name: "fails on path with nonexistant file", - userInput: "purl:tttt/empty.txt", - wantErr: require.Error, }, { - name: "fails on invalid path", - userInput: "purl:~&&", - wantErr: require.Error, - }, - { - name: "allow empty purl file", - userInput: "purl:test-fixtures/purl/empty.json", - sbom: &sbom.SBOM{}, + name: "do not include namespace when given blank input blank", + userInput: "pkg:golang/wazuh@v4.5.0", context: Context{ Source: &source.Description{ - Metadata: PURLFileMetadata{ - Path: "test-fixtures/purl/empty.json", - }, + Metadata: PURLLiteralMetadata{PURL: "pkg:golang/wazuh@v4.5.0"}, + }, + }, + pkgs: []Package{ + { + Name: "wazuh", + Version: "v4.5.0", + Type: pkg.GoModulePkg, + PURL: "pkg:golang/wazuh@v4.5.0", }, }, }, { - name: "fails on invalid purl in file", + name: "fails on purl list input", userInput: "purl:test-fixtures/purl/invalid-purl.txt", wantErr: require.Error, }, - { - name: "fails on invalid cpe in file", - userInput: "purl:test-fixtures/purl/invalid-cpe.txt", - wantErr: require.Error, - }, { name: "invalid prefix", userInput: "dir:test-fixtures/purl", @@ -509,22 +268,14 @@ func Test_PurlProvider(t *testing.T) { }, } - opts := []cmp.Option{ - cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Metadata", "Language", "CPEs"), - } - - syftPkgOpts := []cmp.Option{ - cmpopts.IgnoreFields(pkg.Package{}, "id"), - cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{}), - } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.wantErr == nil { tc.wantErr = require.NoError } - packages, ctx, gotSBOM, err := purlProvider(tc.userInput) + packages, ctx, _, err := purlProvider(tc.userInput, ProviderConfig{}) + setContextDistro(packages, &ctx) tc.wantErr(t, err) if err != nil { @@ -532,35 +283,20 @@ func Test_PurlProvider(t *testing.T) { return } - if d := cmp.Diff(tc.context, ctx, opts...); d != "" { + if d := cmp.Diff(tc.context, ctx, diffOpts...); d != "" { t.Errorf("unexpected context (-want +got):\n%s", d) } require.Len(t, packages, len(tc.pkgs)) for idx, expected := range tc.pkgs { - if d := cmp.Diff(expected, packages[idx], opts...); d != "" { + if d := cmp.Diff(expected, packages[idx], diffOpts...); d != "" { t.Errorf("unexpected context (-want +got):\n%s", d) } } - - gotSyftPkgs := gotSBOM.Artifacts.Packages.Sorted() - wantSyftPkgs := tc.sbom.Artifacts.Packages.Sorted() - require.Equal(t, len(gotSyftPkgs), len(wantSyftPkgs)) - for idx, wantPkg := range wantSyftPkgs { - if d := cmp.Diff(wantPkg, gotSyftPkgs[idx], syftPkgOpts...); d != "" { - t.Errorf("unexpected Syft Pkg (-want +got):\n%s", d) - } - } - - wantSyftDistro := tc.sbom.Artifacts.LinuxDistribution - gotDistro := gotSBOM.Artifacts.LinuxDistribution - if wantSyftDistro == nil { - require.Nil(t, gotDistro) - return - } - - if d := cmp.Diff(wantSyftDistro, gotDistro); d != "" { - t.Errorf("unexpected Syft Distro (-want +got):\n%s", d) - } }) } } + +var diffOpts = []cmp.Option{ + cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Language", "CPEs"), + cmpopts.IgnoreUnexported(distro.Distro{}), +} diff --git a/grype/pkg/qualifier/rpmmodularity/qualifier_test.go b/grype/pkg/qualifier/rpmmodularity/qualifier_test.go index f8b14f1a175..994876bf760 100644 --- a/grype/pkg/qualifier/rpmmodularity/qualifier_test.go +++ b/grype/pkg/qualifier/rpmmodularity/qualifier_test.go @@ -11,7 +11,7 @@ import ( ) func TestRpmModularity_Satisfied(t *testing.T) { - oracle, _ := distro.New(distro.OracleLinux, "8") + oracle, _ := distro.New(distro.OracleLinux, "8", "") tests := []struct { name string diff --git a/grype/pkg/syft_provider.go b/grype/pkg/syft_provider.go index 8cfaec2de1f..9bd8a609cd1 100644 --- a/grype/pkg/syft_provider.go +++ b/grype/pkg/syft_provider.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/anchore/go-collections" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/stereoscope" "github.com/anchore/stereoscope/pkg/image" @@ -19,14 +20,7 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, if err != nil { return nil, Context{}, nil, err } - - defer func() { - if src != nil { - if err := src.Close(); err != nil { - log.Tracef("unable to close source: %+v", err) - } - } - }() + defer log.CloseAndLogError(src, "syft source") s, err := syft.CreateSBOM(context.Background(), src, config.SBOMOptions) if err != nil { @@ -37,14 +31,16 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, return nil, Context{}, nil, errors.New("no SBOM provided") } - pkgCatalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, s.Artifacts.LinuxDistribution) - srcDescription := src.Describe() + d := distro.FromRelease(s.Artifacts.LinuxDistribution) + + pkgCatalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d) + packages := FromCollection(pkgCatalog, config.SynthesisConfig) pkgCtx := Context{ Source: &srcDescription, - Distro: s.Artifacts.LinuxDistribution, + Distro: d, } return packages, pkgCtx, s, nil diff --git a/grype/pkg/syft_sbom_provider.go b/grype/pkg/syft_sbom_provider.go index 056742a94a9..f2ebf9bbd48 100644 --- a/grype/pkg/syft_sbom_provider.go +++ b/grype/pkg/syft_sbom_provider.go @@ -9,68 +9,72 @@ import ( "strings" "github.com/gabriel-vasile/mimetype" - "github.com/mitchellh/go-homedir" + "github.com/anchore/go-homedir" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft/format/syftjson" "github.com/anchore/syft/syft/sbom" ) +type SBOMFileMetadata struct { + Path string +} + func syftSBOMProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { - s, err := getSBOM(userInput) + s, fmtID, path, err := getSBOM(userInput) if err != nil { return nil, Context{}, nil, err } - catalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, s.Artifacts.LinuxDistribution) + src := s.Source + if src.Metadata == nil && path != "" { + src.Metadata = SBOMFileMetadata{ + Path: path, + } + } + + d := distro.FromRelease(s.Artifacts.LinuxDistribution) - return FromCollection(catalog, config.SynthesisConfig), Context{ - Source: &s.Source, - Distro: s.Artifacts.LinuxDistribution, - }, s, nil -} + catalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d) -func newInputInfo(scheme, contentTye string) *inputInfo { - return &inputInfo{ - Scheme: scheme, - ContentType: contentTye, + var enhancers []Enhancer + if fmtID != syftjson.ID { + enhancers = purlEnhancers } -} -type inputInfo struct { - ContentType string - Scheme string + return FromCollection(catalog, config.SynthesisConfig, enhancers...), Context{ + Source: &src, + Distro: d, + }, s, nil } -func getSBOM(userInput string) (*sbom.SBOM, error) { - reader, err := getSBOMReader(userInput) +func getSBOM(userInput string) (*sbom.SBOM, sbom.FormatID, string, error) { + reader, path, err := getSBOMReader(userInput) if err != nil { - return nil, err + return nil, "", path, err } + s, fmtID, err := readSBOM(reader) + return s, fmtID, path, err +} + +func readSBOM(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, error) { s, fmtID, _, err := format.Decode(reader) if err != nil { - return nil, fmt.Errorf("unable to decode sbom: %w", err) + return nil, "", fmt.Errorf("unable to decode sbom: %w", err) } if fmtID == "" || s == nil { - return nil, errDoesNotProvide + return nil, "", errDoesNotProvide } - return s, nil + return s, fmtID, nil } -func getSBOMReader(userInput string) (r io.ReadSeeker, err error) { - r, _, err = extractReaderAndInfo(userInput) - if err != nil { - return nil, err - } - - return r, nil -} - -func extractReaderAndInfo(userInput string) (io.ReadSeeker, *inputInfo, error) { +func getSBOMReader(userInput string) (io.ReadSeeker, string, error) { switch { // the order of cases matter case userInput == "": @@ -78,44 +82,39 @@ func extractReaderAndInfo(userInput string) (io.ReadSeeker, *inputInfo, error) { // options from the CLI, otherwise we should not assume there is any valid input from stdin. r, err := stdinReader() if err != nil { - return nil, nil, err + return nil, "", err } return decodeStdin(r) + case explicitlySpecifyingPurlList(userInput): + filepath := strings.TrimPrefix(userInput, purlInputPrefix) + return openFile(filepath) + case explicitlySpecifyingSBOM(userInput): filepath := strings.TrimPrefix(userInput, "sbom:") - return parseSBOM("sbom", filepath) + return openFile(filepath) case isPossibleSBOM(userInput): - return parseSBOM("", userInput) + return openFile(userInput) default: - return nil, nil, errDoesNotProvide + return nil, "", errDoesNotProvide } } -func parseSBOM(scheme, path string) (io.ReadSeeker, *inputInfo, error) { - r, err := openFile(path) - if err != nil { - return nil, nil, err - } - info := newInputInfo(scheme, "sbom") - return r, info, nil -} - -func decodeStdin(r io.Reader) (io.ReadSeeker, *inputInfo, error) { +func decodeStdin(r io.Reader) (io.ReadSeeker, string, error) { b, err := io.ReadAll(r) if err != nil { - return nil, nil, fmt.Errorf("failed reading stdin: %w", err) + return nil, "", fmt.Errorf("failed reading stdin: %w", err) } reader := bytes.NewReader(b) _, err = reader.Seek(0, io.SeekStart) if err != nil { - return nil, nil, fmt.Errorf("failed to parse stdin: %w", err) + return nil, "", fmt.Errorf("failed to parse stdin: %w", err) } - return reader, newInputInfo("", "sbom"), nil + return reader, "", nil } func stdinReader() (io.Reader, error) { @@ -131,37 +130,26 @@ func stdinReader() (io.Reader, error) { return os.Stdin, nil } -func closeFile(f *os.File) { - if f == nil { - return - } - - err := f.Close() - if err != nil { - log.Warnf("failed to close file %s: %v", f.Name(), err) - } -} - -func openFile(path string) (*os.File, error) { +func openFile(path string) (io.ReadSeekCloser, string, error) { expandedPath, err := homedir.Expand(path) if err != nil { - return nil, fmt.Errorf("unable to open SBOM: %w", err) + return nil, path, fmt.Errorf("unable to open SBOM: %w", err) } f, err := os.Open(expandedPath) if err != nil { - return nil, fmt.Errorf("unable to open file %s: %w", expandedPath, err) + return nil, path, fmt.Errorf("unable to open file %s: %w", expandedPath, err) } - return f, nil + return f, path, nil } func isPossibleSBOM(userInput string) bool { - f, err := openFile(userInput) + f, path, err := openFile(userInput) if err != nil { return false } - defer closeFile(f) + defer log.CloseAndLogError(f, path) mType, err := mimetype.DetectReader(f) if err != nil { @@ -185,3 +173,7 @@ func isAncestorOfMimetype(mType *mimetype.MIME, expected string) bool { func explicitlySpecifyingSBOM(userInput string) bool { return strings.HasPrefix(userInput, "sbom:") } + +func explicitlySpecifyingPurlList(userInput string) bool { + return strings.HasPrefix(userInput, purlInputPrefix) +} diff --git a/grype/pkg/syft_sbom_provider_test.go b/grype/pkg/syft_sbom_provider_test.go index 2d2e48614d1..5d18d06570c 100644 --- a/grype/pkg/syft_sbom_provider_test.go +++ b/grype/pkg/syft_sbom_provider_test.go @@ -1,15 +1,19 @@ package pkg import ( + "slices" "strings" "testing" "github.com/go-test/deep" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -219,8 +223,8 @@ func TestParseSyftJSON(t *testing.T) { }, }, }, - Distro: &linux.Release{ - Name: "alpine", + Distro: &distro.Distro{ + Type: "alpine", Version: "3.12.0", }, }, @@ -337,9 +341,184 @@ var springImageTestCase = struct { RepoDigests: []string{"springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08"}, }, }, - Distro: &linux.Release{ - Name: "debian", + Distro: &distro.Distro{ + Type: "debian", Version: "9", }, }, } + +func Test_PurlList(t *testing.T) { + tests := []struct { + name string + userInput string + context Context + pkgs []Package + wantErr require.ErrorAssertionFunc + }{ + { + name: "takes multiple purls", + userInput: "purl:test-fixtures/purl/valid-purl.txt", + context: Context{ + Distro: &distro.Distro{ + Type: "debian", + IDLike: []string{"debian"}, + Version: "8", + }, + Source: &source.Description{ + Metadata: SBOMFileMetadata{ + Path: "test-fixtures/purl/valid-purl.txt", + }, + }, + }, + pkgs: []Package{ + { + Name: "ant", + Version: "1.10.8", + Type: pkg.JavaPkg, + PURL: "pkg:maven/org.apache.ant/ant@1.10.8", + Metadata: JavaMetadata{ + PomArtifactID: "ant", + PomGroupID: "org.apache.ant", + }, + }, + { + Name: "log4j-core", + Version: "2.14.1", + Type: pkg.JavaPkg, + PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", + Metadata: JavaMetadata{ + PomArtifactID: "log4j-core", + PomGroupID: "org.apache.logging.log4j", + }, + }, + { + Name: "sysv-rc", + Version: "2.88dsf-59", + Type: pkg.DebPkg, + PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit", + Distro: &distro.Distro{Type: distro.Debian, Version: "8", Codename: "", IDLike: []string{"debian"}}, + Upstreams: []UpstreamPackage{ + { + Name: "sysvinit", + }, + }, + }, + }, + }, + { + name: "infer context when distro is present for multiple similar purls", + userInput: "purl:test-fixtures/purl/homogeneous-os.txt", + context: Context{ + Distro: &distro.Distro{ + Type: "alpine", + IDLike: []string{"alpine"}, + Version: "3.20.3", + }, + Source: &source.Description{ + Metadata: SBOMFileMetadata{ + Path: "test-fixtures/purl/homogeneous-os.txt", + }, + }, + }, + pkgs: []Package{ + { + Name: "openssl", + Version: "3.2.1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", + Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, + }, + { + Name: "curl", + Version: "7.61.1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", + Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, + }, + }, + }, + { + name: "different distro info in purls does not infer context", + userInput: "purl:test-fixtures/purl/different-os.txt", + context: Context{ + // important: no distro info inferred + Source: &source.Description{ + Metadata: SBOMFileMetadata{ + Path: "test-fixtures/purl/different-os.txt", + }, + }, + }, + pkgs: []Package{ + { + Name: "openssl", + Version: "3.2.1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", + Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, + }, + { + Name: "curl", + Version: "7.61.1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2", + Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.2", Codename: "", IDLike: []string{"alpine"}}, + }, + }, + }, + { + name: "fails on path with nonexistant file", + userInput: "purl:tttt/empty.txt", + wantErr: require.Error, + }, + { + name: "fails on invalid path", + userInput: "purl:~&&", + wantErr: require.Error, + }, + { + name: "fails for empty purl file", + userInput: "purl:test-fixtures/purl/empty.json", + wantErr: require.Error, + }, + { + name: "fails on invalid purl in file", + userInput: "purl:test-fixtures/purl/invalid-purl.txt", + wantErr: require.Error, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.wantErr == nil { + tc.wantErr = require.NoError + } + + packages, ctx, _, err := Provide(tc.userInput, ProviderConfig{}) + + tc.wantErr(t, err) + if err != nil { + require.Nil(t, packages) + return + } + + if d := cmp.Diff(tc.context, ctx, diffOpts...); d != "" { + t.Errorf("unexpected context (-want +got):\n%s", d) + } + require.Len(t, packages, len(tc.pkgs)) + + slices.SortFunc(packages, func(a, b Package) int { + return strings.Compare(a.Name, b.Name) + }) + slices.SortFunc(tc.pkgs, func(a, b Package) int { + return strings.Compare(a.Name, b.Name) + }) + + for idx, expected := range tc.pkgs { + if d := cmp.Diff(expected, packages[idx], diffOpts...); d != "" { + t.Errorf("unexpected context (-want +got):\n%s", d) + } + } + }) + } +} diff --git a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden index 4c8e813faf5..34638ea5544 100644 --- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden +++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden @@ -2,10 +2,10 @@ "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.6", - "serialNumber": "urn:uuid:d78e14f4-ade8-4948-991c-73318b661114", + "serialNumber": "urn:uuid:c7b9f230-9fb8-43f7-af7a-824cd878a853", "version": 1, "metadata": { - "timestamp": "2025-02-27T13:15:13-05:00", + "timestamp": "2025-05-16T15:26:00-04:00", "tools": { "components": [ { @@ -19,12 +19,12 @@ "component": { "bom-ref": "163686ac6e30c752", "type": "file", - "name": "/var/folders/c0/4y79v5k56bz8v34chcmvq2k80000gp/T/TestCycloneDxPresenterDir3112352663/001" + "name": "/var/folders/8y/ct5nbgtj4p30k10kfmq4p4s00000gn/T/TestCycloneDxPresenterDir76562573/001" } }, "components": [ { - "bom-ref": "9baa2db122fea516", + "bom-ref": "bbb0ba712c2b94ea", "type": "library", "name": "package-1", "version": "1.1.1", @@ -57,7 +57,7 @@ ] }, { - "bom-ref": "pkg:deb/package-2@2.2.2?package-id=7bb53d560434bc7f", + "bom-ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625", "type": "library", "name": "package-2", "version": "2.2.2", @@ -89,60 +89,50 @@ ], "vulnerabilities": [ { - "bom-ref": "urn:uuid:e47db483-7ab2-4177-a82b-416b5a37bf93", + "bom-ref": "urn:uuid:983f094f-a20c-4aed-b6f1-d9b417c706cc", "id": "CVE-1999-0001", - "source": { - "name": "source-1" - }, + "source": {}, "references": [ { "id": "CVE-1999-0001", - "source": { - "name": "source-1" - } + "source": {} } ], "ratings": [ { - "score": 4, + "score": 8.2, "severity": "low", - "method": "CVSSv3", - "vector": "another vector" + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H" } ], - "description": "1999-01 description", "affects": [ { - "ref": "9baa2db122fea516" + "ref": "bbb0ba712c2b94ea" } ] }, { - "bom-ref": "urn:uuid:3e3fbcf0-8dda-41a7-97ed-d1122923bf79", + "bom-ref": "urn:uuid:ae00e132-f2c5-4d07-896d-31aaf91e04d1", "id": "CVE-1999-0002", - "source": { - "name": "source-2" - }, + "source": {}, "references": [ { "id": "CVE-1999-0002", - "source": { - "name": "source-2" - } + "source": {} } ], "ratings": [ { - "score": 1, + "score": 8.5, "severity": "critical", - "method": "CVSSv2", - "vector": "vector" + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H" } ], - "description": "1999-02 description", "affects": [ { - "ref": "pkg:deb/package-2@2.2.2?package-id=7bb53d560434bc7f" + "ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625" } ] } diff --git a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden index f5676aedbb4..bafdf481d93 100644 --- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden +++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden @@ -2,10 +2,10 @@ "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.6", - "serialNumber": "urn:uuid:24d4fb73-8881-462c-940a-61b6d37b579d", + "serialNumber": "urn:uuid:e9977dcd-e35e-4053-ab49-932f99e4d240", "version": 1, "metadata": { - "timestamp": "2025-02-27T13:15:13-05:00", + "timestamp": "2025-05-16T15:26:00-04:00", "tools": { "components": [ { @@ -25,7 +25,7 @@ }, "components": [ { - "bom-ref": "9baa2db122fea516", + "bom-ref": "bbb0ba712c2b94ea", "type": "library", "name": "package-1", "version": "1.1.1", @@ -58,7 +58,7 @@ ] }, { - "bom-ref": "pkg:deb/package-2@2.2.2?package-id=7bb53d560434bc7f", + "bom-ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625", "type": "library", "name": "package-2", "version": "2.2.2", @@ -90,60 +90,50 @@ ], "vulnerabilities": [ { - "bom-ref": "urn:uuid:638bc45b-a838-4017-b690-dc5d8d3cfe8d", + "bom-ref": "urn:uuid:9f6e21d3-d245-4ee3-9558-cb5cc4936b32", "id": "CVE-1999-0001", - "source": { - "name": "source-1" - }, + "source": {}, "references": [ { "id": "CVE-1999-0001", - "source": { - "name": "source-1" - } + "source": {} } ], "ratings": [ { - "score": 4, + "score": 8.2, "severity": "low", - "method": "CVSSv3", - "vector": "another vector" + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H" } ], - "description": "1999-01 description", "affects": [ { - "ref": "9baa2db122fea516" + "ref": "bbb0ba712c2b94ea" } ] }, { - "bom-ref": "urn:uuid:6592d365-1128-49a0-8a68-570d8188965a", + "bom-ref": "urn:uuid:4fc03529-3b07-41e2-9bc9-c0b2989ccf8e", "id": "CVE-1999-0002", - "source": { - "name": "source-2" - }, + "source": {}, "references": [ { "id": "CVE-1999-0002", - "source": { - "name": "source-2" - } + "source": {} } ], "ratings": [ { - "score": 1, + "score": 8.5, "severity": "critical", - "method": "CVSSv2", - "vector": "vector" + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H" } ], - "description": "1999-02 description", "affects": [ { - "ref": "pkg:deb/package-2@2.2.2?package-id=7bb53d560434bc7f" + "ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625" } ] } diff --git a/grype/presenter/internal/test_helpers.go b/grype/presenter/internal/test_helpers.go index 8c3e8fb12cc..aa2b528abb8 100644 --- a/grype/presenter/internal/test_helpers.go +++ b/grype/presenter/internal/test_helpers.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/clio" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" @@ -15,7 +16,6 @@ import ( "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" syftSource "github.com/anchore/syft/syft/source" @@ -99,7 +99,7 @@ func Redact(s []byte) []byte { return s } -func generateMatches(t *testing.T, p1, p2 pkg.Package) match.Matches { +func generateMatches(t *testing.T, p1, p2 pkg.Package) match.Matches { // nolint:funlen t.Helper() matches := []match.Match{ @@ -114,6 +114,29 @@ func generateMatches(t *testing.T, p1, p2 pkg.Package) match.Matches { Versions: []string{"1.2.1", "2.1.3", "3.4.0"}, State: vulnerability.FixStateFixed, }, + Metadata: &vulnerability.Metadata{ + ID: "CVE-1999-0001", + Severity: "Low", + Cvss: []vulnerability.Cvss{ + { + Source: "nvd", + Type: "CVSS", + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", + Metrics: vulnerability.CvssMetrics{ + BaseScore: 8.2, + }, + }, + }, + KnownExploited: nil, + EPSS: []vulnerability.EPSS{ + { + CVE: "CVE-1999-0001", + EPSS: 0.03, + Percentile: 0.42, + }, + }, + }, }, Package: p1, Details: []match.Detail{ @@ -139,6 +162,34 @@ func generateMatches(t *testing.T, p1, p2 pkg.Package) match.Matches { ID: "CVE-1999-0002", Namespace: "source-2", }, + Metadata: &vulnerability.Metadata{ + ID: "CVE-1999-0002", + Severity: "Critical", + Cvss: []vulnerability.Cvss{ + { + Source: "nvd", + Type: "CVSS", + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", + Metrics: vulnerability.CvssMetrics{ + BaseScore: 8.5, + }, + }, + }, + KnownExploited: []vulnerability.KnownExploited{ + { + CVE: "CVE-1999-0002", + KnownRansomwareCampaignUse: "Known", + }, + }, + EPSS: []vulnerability.EPSS{ + { + CVE: "CVE-1999-0002", + EPSS: 0.08, + Percentile: 0.53, + }, + }, + }, }, Package: p2, Details: []match.Detail{ @@ -173,6 +224,29 @@ func generateIgnoredMatches(t *testing.T, p pkg.Package) []match.IgnoredMatch { ID: "CVE-1999-0001", Namespace: "source-1", }, + Metadata: &vulnerability.Metadata{ + ID: "CVE-1999-0001", + Severity: "Low", + Cvss: []vulnerability.Cvss{ + { + Source: "nvd", + Type: "CVSS", + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", + Metrics: vulnerability.CvssMetrics{ + BaseScore: 8.2, + }, + }, + }, + KnownExploited: nil, + EPSS: []vulnerability.EPSS{ + { + CVE: "CVE-1999-0001", + EPSS: 0.03, + Percentile: 0.42, + }, + }, + }, }, Package: p, Details: []match.Detail{ @@ -200,6 +274,34 @@ func generateIgnoredMatches(t *testing.T, p pkg.Package) []match.IgnoredMatch { ID: "CVE-1999-0002", Namespace: "source-2", }, + Metadata: &vulnerability.Metadata{ + ID: "CVE-1999-0002", + Severity: "Critical", + Cvss: []vulnerability.Cvss{ + { + Source: "nvd", + Type: "CVSS", + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", + Metrics: vulnerability.CvssMetrics{ + BaseScore: 8.5, + }, + }, + }, + KnownExploited: []vulnerability.KnownExploited{ + { + CVE: "CVE-1999-0002", + KnownRansomwareCampaignUse: "Known", + }, + }, + EPSS: []vulnerability.EPSS{ + { + CVE: "CVE-1999-0002", + EPSS: 0.08, + Percentile: 0.53, + }, + }, + }, }, Package: p, Details: []match.Detail{ @@ -224,6 +326,28 @@ func generateIgnoredMatches(t *testing.T, p pkg.Package) []match.IgnoredMatch { ID: "CVE-1999-0004", Namespace: "source-2", }, + Metadata: &vulnerability.Metadata{ + ID: "CVE-1999-0004", + Severity: "High", + Cvss: []vulnerability.Cvss{ + { + Source: "nvd", + Type: "CVSS", + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:L", + Metrics: vulnerability.CvssMetrics{ + BaseScore: 7.2, + }, + }, + }, + EPSS: []vulnerability.EPSS{ + { + CVE: "CVE-1999-0004", + EPSS: 0.03, + Percentile: 0.75, + }, + }, + }, }, Package: p, Details: []match.Detail{ @@ -382,8 +506,8 @@ func generateContext(t *testing.T, scheme SyftSource) pkg.Context { return pkg.Context{ Source: &desc, - Distro: &linux.Release{ - Name: "centos", + Distro: &distro.Distro{ + Type: "centos", IDLike: []string{ "centos", }, diff --git a/grype/presenter/json/presenter.go b/grype/presenter/json/presenter.go index 03d46c67042..27ca2a912d5 100644 --- a/grype/presenter/json/presenter.go +++ b/grype/presenter/json/presenter.go @@ -4,25 +4,18 @@ import ( "encoding/json" "io" - "github.com/anchore/clio" "github.com/anchore/grype/grype/presenter/models" ) type Presenter struct { - id clio.Identification - document models.Document - appConfig interface{} - dbStatus interface{} - pretty bool + document models.Document + pretty bool } func NewPresenter(pb models.PresenterConfig) *Presenter { return &Presenter{ - id: pb.ID, - document: pb.Document, - appConfig: pb.AppConfig, - dbStatus: pb.DBStatus, - pretty: pb.Pretty, + document: pb.Document, + pretty: pb.Pretty, } } diff --git a/grype/presenter/json/presenter_test.go b/grype/presenter/json/presenter_test.go index 3910b55ce9e..dc2929ae518 100644 --- a/grype/presenter/json/presenter_test.go +++ b/grype/presenter/json/presenter_test.go @@ -12,11 +12,11 @@ import ( "github.com/anchore/clio" "github.com/anchore/go-testutils" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" ) @@ -85,8 +85,8 @@ func TestEmptyJsonPresenter(t *testing.T) { ctx := pkg.Context{ Source: &source.Description{}, - Distro: &linux.Release{ - ID: "centos", + Distro: &distro.Distro{ + Type: "centos", IDLike: []string{"rhel"}, Version: "8.0", }, diff --git a/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden b/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden index 05f412100c6..d77afa9e331 100644 --- a/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden +++ b/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden @@ -4,20 +4,28 @@ "vulnerability": { "id": "CVE-1999-0001", "dataSource": "", - "namespace": "source-1", "severity": "Low", "urls": [], - "description": "1999-01 description", "cvss": [ { - "version": "3.0", - "vector": "another vector", + "source": "nvd", + "type": "CVSS", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", "metrics": { - "baseScore": 4 + "baseScore": 8.2 }, "vendorMetadata": {} } ], + "epss": [ + { + "cve": "CVE-1999-0001", + "epss": 0.03, + "percentile": 0.42, + "date": "0001-01-01" + } + ], "fix": { "versions": [ "1.2.1", @@ -26,7 +34,8 @@ ], "state": "fixed" }, - "advisories": [] + "advisories": [], + "risk": 1.68 }, "relatedVulnerabilities": [], "matchDetails": [ @@ -48,7 +57,7 @@ } ], "artifact": { - "id": "9baa2db122fea516", + "id": "bbb0ba712c2b94ea", "name": "package-1", "version": "1.1.1", "type": "rpm", @@ -76,30 +85,40 @@ "vulnerability": { "id": "CVE-1999-0002", "dataSource": "", - "namespace": "source-2", "severity": "Critical", "urls": [], - "description": "1999-02 description", "cvss": [ { - "version": "2.0", - "vector": "vector", + "source": "nvd", + "type": "CVSS", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", "metrics": { - "baseScore": 1, - "exploitabilityScore": 2, - "impactScore": 3 + "baseScore": 8.5 }, - "vendorMetadata": { - "BaseSeverity": "Low", - "Status": "verified" - } + "vendorMetadata": {} + } + ], + "knownExploited": [ + { + "cve": "CVE-1999-0002", + "knownRansomwareCampaignUse": "Known" + } + ], + "epss": [ + { + "cve": "CVE-1999-0002", + "epss": 0.08, + "percentile": 0.53, + "date": "0001-01-01" } ], "fix": { "versions": [], "state": "" }, - "advisories": [] + "advisories": [], + "risk": 96.25000000000001 }, "relatedVulnerabilities": [], "matchDetails": [ @@ -115,7 +134,7 @@ } ], "artifact": { - "id": "7bb53d560434bc7f", + "id": "74378afe15713625", "name": "package-2", "version": "2.2.2", "type": "deb", diff --git a/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden b/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden index adb4ab0de50..b49085fb678 100644 --- a/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden +++ b/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden @@ -4,20 +4,28 @@ "vulnerability": { "id": "CVE-1999-0001", "dataSource": "", - "namespace": "source-1", "severity": "Low", "urls": [], - "description": "1999-01 description", "cvss": [ { - "version": "3.0", - "vector": "another vector", + "source": "nvd", + "type": "CVSS", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", "metrics": { - "baseScore": 4 + "baseScore": 8.2 }, "vendorMetadata": {} } ], + "epss": [ + { + "cve": "CVE-1999-0001", + "epss": 0.03, + "percentile": 0.42, + "date": "0001-01-01" + } + ], "fix": { "versions": [ "1.2.1", @@ -26,7 +34,8 @@ ], "state": "fixed" }, - "advisories": [] + "advisories": [], + "risk": 1.68 }, "relatedVulnerabilities": [], "matchDetails": [ @@ -48,7 +57,7 @@ } ], "artifact": { - "id": "9baa2db122fea516", + "id": "bbb0ba712c2b94ea", "name": "package-1", "version": "1.1.1", "type": "rpm", @@ -76,30 +85,40 @@ "vulnerability": { "id": "CVE-1999-0002", "dataSource": "", - "namespace": "source-2", "severity": "Critical", "urls": [], - "description": "1999-02 description", "cvss": [ { - "version": "2.0", - "vector": "vector", + "source": "nvd", + "type": "CVSS", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", "metrics": { - "baseScore": 1, - "exploitabilityScore": 2, - "impactScore": 3 + "baseScore": 8.5 }, - "vendorMetadata": { - "BaseSeverity": "Low", - "Status": "verified" - } + "vendorMetadata": {} + } + ], + "knownExploited": [ + { + "cve": "CVE-1999-0002", + "knownRansomwareCampaignUse": "Known" + } + ], + "epss": [ + { + "cve": "CVE-1999-0002", + "epss": 0.08, + "percentile": 0.53, + "date": "0001-01-01" } ], "fix": { "versions": [], "state": "" }, - "advisories": [] + "advisories": [], + "risk": 96.25000000000001 }, "relatedVulnerabilities": [], "matchDetails": [ @@ -115,7 +134,7 @@ } ], "artifact": { - "id": "7bb53d560434bc7f", + "id": "74378afe15713625", "name": "package-2", "version": "2.2.2", "type": "deb", diff --git a/grype/presenter/models/descriptor.go b/grype/presenter/models/descriptor.go index 33cfb6ad2ef..e604fb1142f 100644 --- a/grype/presenter/models/descriptor.go +++ b/grype/presenter/models/descriptor.go @@ -2,9 +2,9 @@ package models // descriptor describes what created the document as well as surrounding metadata type descriptor struct { - Name string `json:"name"` - Version string `json:"version"` - Configuration interface{} `json:"configuration,omitempty"` - VulnerabilityDBStatus interface{} `json:"db,omitempty"` - Timestamp string `json:"timestamp"` + Name string `json:"name"` + Version string `json:"version"` + Configuration any `json:"configuration,omitempty"` + DB any `json:"db,omitempty"` + Timestamp string `json:"timestamp"` } diff --git a/grype/presenter/models/distribution.go b/grype/presenter/models/distribution.go index 7e5bf242e39..a8b9d792afa 100644 --- a/grype/presenter/models/distribution.go +++ b/grype/presenter/models/distribution.go @@ -2,8 +2,6 @@ package models import ( "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/internal/log" - "github.com/anchore/syft/syft/linux" ) // distribution provides information about a detected Linux distribution. @@ -14,27 +12,14 @@ type distribution struct { } // newDistribution creates a struct with the Linux distribution to be represented in JSON. -func newDistribution(r *linux.Release) distribution { - if r == nil { +func newDistribution(d *distro.Distro) distribution { + if d == nil { return distribution{} } - // attempt to use the strong distro type (like the matchers do) - d, err := distro.NewFromRelease(*r) - if err != nil { - log.Warnf("unable to determine linux distribution: %+v", err) - - // as a fallback use the raw release information - return distribution{ - Name: r.ID, - Version: r.VersionID, - IDLike: cleanIDLike(r.IDLike), - } - } - return distribution{ Name: d.Name(), - Version: d.FullVersion(), + Version: d.Version, IDLike: cleanIDLike(d.IDLike), } } diff --git a/grype/presenter/models/document.go b/grype/presenter/models/document.go index 93dbadcaada..068486d1eb1 100644 --- a/grype/presenter/models/document.go +++ b/grype/presenter/models/document.go @@ -20,7 +20,7 @@ type Document struct { } // NewDocument creates and populates a new Document struct, representing the populated JSON document. -func NewDocument(id clio.Identification, packages []pkg.Package, context pkg.Context, matches match.Matches, ignoredMatches []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider, appConfig any, dbStatus any, strategy SortStrategy) (Document, error) { +func NewDocument(id clio.Identification, packages []pkg.Package, context pkg.Context, matches match.Matches, ignoredMatches []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider, appConfig any, dbInfo any, strategy SortStrategy) (Document, error) { timestamp, timestampErr := time.Now().Local().MarshalText() if timestampErr != nil { return Document{}, timestampErr @@ -78,11 +78,11 @@ func NewDocument(id clio.Identification, packages []pkg.Package, context pkg.Con Source: src, Distro: newDistribution(context.Distro), Descriptor: descriptor{ - Name: id.Name, - Version: id.Version, - Configuration: appConfig, - VulnerabilityDBStatus: dbStatus, - Timestamp: string(timestamp), + Name: id.Name, + Version: id.Version, + Configuration: appConfig, + DB: dbInfo, + Timestamp: string(timestamp), }, }, nil } diff --git a/grype/presenter/models/document_test.go b/grype/presenter/models/document_test.go index d81f0dc144f..0cb95916935 100644 --- a/grype/presenter/models/document_test.go +++ b/grype/presenter/models/document_test.go @@ -7,10 +7,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/anchore/clio" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" syftSource "github.com/anchore/syft/syft/source" ) @@ -75,8 +75,8 @@ func TestPackagesAreSorted(t *testing.T) { Source: &syftSource.Description{ Metadata: syftSource.DirectoryMetadata{}, }, - Distro: &linux.Release{ - ID: "centos", + Distro: &distro.Distro{ + Type: "centos", IDLike: []string{"rhel"}, Version: "8.0", }, @@ -136,8 +136,8 @@ func TestFixSuggestedVersion(t *testing.T) { Source: &syftSource.Description{ Metadata: syftSource.DirectoryMetadata{}, }, - Distro: &linux.Release{ - ID: "centos", + Distro: &distro.Distro{ + Type: "centos", IDLike: []string{"rhel"}, Version: "8.0", }, diff --git a/grype/presenter/models/presenter_bundle.go b/grype/presenter/models/presenter_bundle.go index cdd99b89cbc..106e963592c 100644 --- a/grype/presenter/models/presenter_bundle.go +++ b/grype/presenter/models/presenter_bundle.go @@ -6,10 +6,8 @@ import ( ) type PresenterConfig struct { - ID clio.Identification - Document Document - SBOM *sbom.SBOM - AppConfig interface{} - DBStatus interface{} - Pretty bool + ID clio.Identification + Document Document + SBOM *sbom.SBOM + Pretty bool } diff --git a/grype/presenter/models/sort.go b/grype/presenter/models/sort.go index 8bb8ee10ab9..f1c5895e44f 100644 --- a/grype/presenter/models/sort.go +++ b/grype/presenter/models/sort.go @@ -10,13 +10,18 @@ import ( type SortStrategy string const ( - SortByPackage SortStrategy = "package" + SortByPackage SortStrategy = "package" + SortBySeverity SortStrategy = "severity" + SortByThreat SortStrategy = "epss" + SortByRisk SortStrategy = "risk" + SortByKEV SortStrategy = "kev" + SortByVulnerability SortStrategy = "vulnerability" - defaultSortStrategy = SortByPackage + DefaultSortStrategy = SortByRisk ) func SortStrategies() []SortStrategy { - return []SortStrategy{SortByPackage} + return []SortStrategy{SortByPackage, SortBySeverity, SortByThreat, SortByRisk, SortByKEV, SortByVulnerability} } func (s SortStrategy) String() string { @@ -37,14 +42,85 @@ type sortStrategyImpl []compareFunc // matchSortStrategy provides predefined sort strategies for Match var matchSortStrategy = map[SortStrategy]sortStrategyImpl{ SortByPackage: { - compareByPackageName, - compareByPackageVersion, - compareByPackageType, + comparePackageAttributes, + compareVulnerabilityAttributes, + }, + SortByVulnerability: { + compareVulnerabilityAttributes, + comparePackageAttributes, + }, + SortBySeverity: { + // severity and tangential attributes... + compareBySeverity, + compareByRisk, + compareByEPSSPercentile, + // followed by package attributes... + comparePackageAttributes, + // followed by the remaining vulnerability attributes... + compareByVulnerabilityID, + }, + SortByThreat: { + // epss and tangential attributes... + compareByEPSSPercentile, + compareByRisk, + compareBySeverity, + // followed by package attributes... + comparePackageAttributes, + // followed by the remaining vulnerability attributes... + compareByVulnerabilityID, + }, + SortByRisk: { + // risk and tangential attributes... + compareByRisk, + compareBySeverity, + compareByEPSSPercentile, + // followed by package attributes... + comparePackageAttributes, + // followed by the remaining vulnerability attributes... + compareByVulnerabilityID, + }, + SortByKEV: { + compareByKEV, + // risk and tangential attributes... + compareByRisk, compareBySeverity, + compareByEPSSPercentile, + // followed by package attributes... + comparePackageAttributes, + // followed by the remaining vulnerability attributes... compareByVulnerabilityID, }, } +func compareVulnerabilityAttributes(a, b Match) int { + return combine( + compareByVulnerabilityID, + compareByRisk, + compareBySeverity, + compareByEPSSPercentile, + )(a, b) +} + +func comparePackageAttributes(a, b Match) int { + return combine( + compareByPackageName, + compareByPackageVersion, + compareByPackageType, + )(a, b) +} + +func combine(impls ...compareFunc) compareFunc { + return func(a, b Match) int { + for _, impl := range impls { + result := impl(a, b) + if result != 0 { + return result + } + } + return 0 + } +} + // SortMatches sorts matches based on a strategy name func SortMatches(matches []Match, strategyName SortStrategy) { sortWithStrategy(matches, getSortStrategy(strategyName)) @@ -53,8 +129,8 @@ func SortMatches(matches []Match, strategyName SortStrategy) { func getSortStrategy(strategyName SortStrategy) sortStrategyImpl { strategy, exists := matchSortStrategy[strategyName] if !exists { - log.WithFields("strategy", strategyName).Debugf("unknown sort strategy, falling back to default of %q", defaultSortStrategy) - strategy = matchSortStrategy[defaultSortStrategy] + log.WithFields("strategy", strategyName).Debugf("unknown sort strategy, falling back to default of %q", DefaultSortStrategy) + strategy = matchSortStrategy[DefaultSortStrategy] } return strategy } @@ -87,8 +163,22 @@ func compareByVulnerabilityID(a, b Match) int { } func compareBySeverity(a, b Match) int { - aScore := severityScore(a.Vulnerability.Severity) - bScore := severityScore(b.Vulnerability.Severity) + aScore := severityPriority(a.Vulnerability.Severity) + bScore := severityPriority(b.Vulnerability.Severity) + + switch { + case aScore < bScore: // higher severity first + return -1 + case aScore > bScore: + return 1 + default: + return 0 + } +} + +func compareByEPSSPercentile(a, b Match) int { + aScore := epssPercentile(a.Vulnerability.EPSS) + bScore := epssPercentile(b.Vulnerability.EPSS) switch { case aScore > bScore: // higher severity first @@ -142,20 +232,61 @@ func compareByPackageType(a, b Match) int { } } -// severityScore maps severity strings to numeric scores for comparison -func severityScore(severity string) int { +func compareByRisk(a, b Match) int { + aRisk := a.Vulnerability.Risk + bRisk := b.Vulnerability.Risk + + switch { + case aRisk > bRisk: + return -1 + case aRisk < bRisk: + return 1 + default: + return 0 + } +} + +func compareByKEV(a, b Match) int { + aKEV := len(a.Vulnerability.KnownExploited) + bKEV := len(b.Vulnerability.KnownExploited) + + switch { + case aKEV > bKEV: + return -1 + case aKEV < bKEV: + return 1 + default: + return 0 + } +} + +func epssPercentile(es []EPSS) float64 { + switch len(es) { + case 0: + return 0.0 + case 1: + return es[0].Percentile + } + sort.Slice(es, func(i, j int) bool { + return es[i].Percentile > es[j].Percentile + }) + return es[0].Percentile +} + +// severityPriority maps severity strings to numeric priority for comparison (the lowest value is most severe) +func severityPriority(severity string) int { switch strings.ToLower(severity) { case "critical": - return 5 + return 1 case "high": - return 4 + return 2 case "medium": return 3 case "low": - return 2 + return 4 case "negligible": - return 1 + return 5 default: - return 0 + return 100 // least severe } } diff --git a/grype/presenter/models/sort_test.go b/grype/presenter/models/sort_test.go index 478550fcab4..e86ae0c3a83 100644 --- a/grype/presenter/models/sort_test.go +++ b/grype/presenter/models/sort_test.go @@ -4,16 +4,154 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestSortMatches(t *testing.T) { - matches := []Match{ +func TestSortStrategies(t *testing.T) { + strategies := SortStrategies() + expected := []SortStrategy{ + SortByPackage, + SortBySeverity, + SortByThreat, + SortByRisk, + SortByKEV, + SortByVulnerability, + } + assert.Equal(t, expected, strategies) +} + +func TestSortStrategyString(t *testing.T) { + assert.Equal(t, "package", SortByPackage.String()) + assert.Equal(t, "severity", SortBySeverity.String()) + assert.Equal(t, "epss", SortByThreat.String()) + assert.Equal(t, "risk", SortByRisk.String()) + assert.Equal(t, "kev", SortByKEV.String()) + assert.Equal(t, "vulnerability", SortByVulnerability.String()) +} + +func TestGetSortStrategy(t *testing.T) { + tests := []struct { + name string + strategyName SortStrategy + expected bool + }{ + { + name: "Valid strategy", + strategyName: SortByPackage, + expected: true, + }, + { + name: "Invalid strategy", + strategyName: "invalid", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + strategy := getSortStrategy(tt.strategyName) + validStrategy, _ := matchSortStrategy[tt.strategyName] + + if tt.expected { + require.NotNil(t, strategy) + assert.Equal(t, validStrategy, strategy) + } else { + // Should fallback to default strategy + assert.NotNil(t, strategy) + assert.Equal(t, matchSortStrategy[DefaultSortStrategy], strategy) + } + }) + } +} + +func TestEPSSPercentile(t *testing.T) { + tests := []struct { + name string + epss []EPSS + expected float64 + }{ + { + name: "Empty slice", + epss: []EPSS{}, + expected: 0.0, + }, + { + name: "Single item", + epss: []EPSS{ + {Percentile: 0.75}, + }, + expected: 0.75, + }, + { + name: "Multiple items, already sorted", + epss: []EPSS{ + {Percentile: 0.95}, + {Percentile: 0.75}, + {Percentile: 0.50}, + }, + expected: 0.95, + }, + { + name: "Multiple items, unsorted", + epss: []EPSS{ + {Percentile: 0.50}, + {Percentile: 0.95}, + {Percentile: 0.75}, + }, + expected: 0.95, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := epssPercentile(tt.epss) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestSeverityPriority(t *testing.T) { + tests := []struct { + severity string + expected int + }{ + {"critical", 1}, + {"CRITICAL", 1}, + {"high", 2}, + {"HIGH", 2}, + {"medium", 3}, + {"MEDIUM", 3}, + {"low", 4}, + {"LOW", 4}, + {"negligible", 5}, + {"NEGLIGIBLE", 5}, + {"unknown", 100}, + {"", 100}, + } + + for _, tt := range tests { + t.Run(tt.severity, func(t *testing.T) { + result := severityPriority(tt.severity) + assert.Equal(t, tt.expected, result) + }) + } +} + +func createTestMatches() []Match { + return []Match{ { + // match 0: medium severity, high risk, high EPSS, no KEV Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-1111", Severity: "medium", + EPSS: []EPSS{ + {Percentile: 0.90}, + }, + KnownExploited: []KnownExploited{}, // empty KEV }, + Risk: 75.0, }, Artifact: Package{ Name: "package-b", @@ -22,11 +160,17 @@ func TestSortMatches(t *testing.T) { }, }, { + // match 1: critical severity, medium risk, medium EPSS, no KEV Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-2222", Severity: "critical", + EPSS: []EPSS{ + {Percentile: 0.70}, + }, + KnownExploited: []KnownExploited{}, // empty KEV }, + Risk: 50.0, }, Artifact: Package{ Name: "package-a", @@ -35,11 +179,19 @@ func TestSortMatches(t *testing.T) { }, }, { + // match 2: high severity, low risk, low EPSS, has KEV Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-3333", Severity: "high", + EPSS: []EPSS{ + {Percentile: 0.30}, + }, + KnownExploited: []KnownExploited{ + {CVE: "CVE-2023-3333", KnownRansomwareCampaignUse: "No"}, + }, // has KEV }, + Risk: 25.0, }, Artifact: Package{ Name: "package-a", @@ -48,11 +200,17 @@ func TestSortMatches(t *testing.T) { }, }, { + // match 3: low severity, very low risk, very low EPSS, no KEV Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-4444", Severity: "low", + EPSS: []EPSS{ + {Percentile: 0.10}, + }, + KnownExploited: []KnownExploited{}, // empty KEV }, + Risk: 10.0, }, Artifact: Package{ Name: "package-c", @@ -61,11 +219,20 @@ func TestSortMatches(t *testing.T) { }, }, { + // match 4: critical severity, very low risk, medium EPSS, has KEV with ransomware Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-5555", Severity: "critical", + EPSS: []EPSS{ + {Percentile: 0.50}, + }, + KnownExploited: []KnownExploited{ + {CVE: "CVE-2023-5555", KnownRansomwareCampaignUse: "Known"}, + {CVE: "CVE-2023-5555", KnownRansomwareCampaignUse: "Known", Product: "Different Product"}, + }, // has multiple KEV entries with ransomware }, + Risk: 5.0, }, Artifact: Package{ Name: "package-a", @@ -74,122 +241,262 @@ func TestSortMatches(t *testing.T) { }, }, } +} - t.Run("SortByPackage", func(t *testing.T) { - testMatches := deepCopyMatches(matches) - SortMatches(testMatches, SortByPackage) - - expected := []Match{ - // package-a with 1.0.0 version, docker type first (alphabetical) - matches[4], // package-a, 1.0.0, docker, critical - matches[2], // package-a, 1.0.0, npm, high - matches[1], // package-a, 2.0.0, docker, critical - matches[0], // package-b, 1.2.0, npm, medium - matches[3], // package-c, 3.1.0, gem, low - } +func TestAllSortStrategies(t *testing.T) { + matches := createTestMatches() - if diff := cmp.Diff(expected, testMatches); diff != "" { - t.Errorf("SortByPackage mismatch (-want +got):\n%s", diff) - } - }) + tests := []struct { + strategy SortStrategy + expected []int // indexes into the original matches slice + }{ + { + strategy: SortByPackage, + expected: []int{4, 2, 1, 0, 3}, // sorted by package name, version, type + }, + { + strategy: SortByVulnerability, + expected: []int{0, 1, 2, 3, 4}, // sorted by vulnerability ID + }, + { + strategy: SortBySeverity, + expected: []int{1, 4, 2, 0, 3}, // sorted by severity: critical, critical, high, medium, low + }, + { + strategy: SortByThreat, + expected: []int{0, 1, 4, 2, 3}, // sorted by EPSS percentile: 0.90, 0.70, 0.50, 0.30, 0.10 + }, + { + strategy: SortByRisk, + expected: []int{0, 1, 2, 3, 4}, // sorted by risk: 75.0, 50.0, 25.0, 10.0, 5.0 + }, + { + strategy: SortByKEV, + expected: []int{4, 2, 0, 1, 3}, // sorted by KEV count: 2, 1, 0, 0, 0 (with ties broken by risk) + }, + } - t.Run("UnknownStrategy", func(t *testing.T) { - testMatches := deepCopyMatches(matches) - // should use default (package) strategy for unknown strategy names - SortMatches(testMatches, "unknown") - - expected := []Match{ - matches[4], // package-a, 1.0.0, docker - matches[2], // package-a, 1.0.0, npm - matches[1], // package-a, 2.0.0, docker - matches[0], // package-b, 1.2.0, npm - matches[3], // package-c, 3.1.0, gem - } + for _, tt := range tests { + t.Run(string(tt.strategy), func(t *testing.T) { + testMatches := deepCopyMatches(matches) + SortMatches(testMatches, tt.strategy) - if diff := cmp.Diff(expected, testMatches); diff != "" { - t.Errorf("Unknown strategy mismatch (-want +got):\n%s", diff) - } - }) + expected := make([]Match, len(tt.expected)) + for i, idx := range tt.expected { + expected[i] = matches[idx] + } + + if diff := cmp.Diff(expected, testMatches); diff != "" { + t.Errorf("%s mismatch (-want +got):\n%s", tt.strategy, diff) + } + }) + } } -func TestEdgeCases(t *testing.T) { - t.Run("EmptySlice", func(t *testing.T) { - matches := []Match{} - // should not panic on empty slice - SortMatches(matches, SortByPackage) +func TestIndividualCompareFunctions(t *testing.T) { + ms := createTestMatches() + m0 := ms[0] // medium severity, high risk, high EPSS, no KEV + m1 := ms[1] // critical severity, medium risk, medium EPSS, no KEV + m2 := ms[2] // high severity, low risk, low EPSS, has KEV + m3 := ms[3] // low severity, very low risk, very low EPSS, no KEV + m4 := ms[4] // critical severity, very low risk, medium EPSS, has KEV with ransomware - expected := []Match{} - if diff := cmp.Diff(expected, matches); diff != "" { - t.Errorf("Empty slice mismatch (-want +got):\n%s", diff) + tests := []struct { + name string + compareFunc compareFunc + pairs []struct { + a, b Match + expected int } + }{ + { + name: "compareByVulnerabilityID", + compareFunc: compareByVulnerabilityID, + pairs: []struct { + a, b Match + expected int + }{ + {m0, m1, -1}, // CVE-2023-1111 < CVE-2023-2222 + {m1, m0, 1}, // CVE-2023-2222 > CVE-2023-1111 + {m0, m0, 0}, // Same ID + }, + }, + { + name: "compareBySeverity", + compareFunc: compareBySeverity, + pairs: []struct { + a, b Match + expected int + }{ + {m0, m1, 1}, // medium > critical + {m1, m0, -1}, // critical < medium + {m1, m4, 0}, // both critical + {m2, m3, -1}, // high < low + }, + }, + { + name: "compareByEPSSPercentile", + compareFunc: compareByEPSSPercentile, + pairs: []struct { + a, b Match + expected int + }{ + {m0, m1, -1}, // 0.90 > 0.70 + {m1, m0, 1}, // 0.70 < 0.90 + {m1, m4, -1}, // 0.70 > 0.50 + {m4, m1, 1}, // 0.50 < 0.70 + }, + }, + { + name: "compareByPackageName", + compareFunc: compareByPackageName, + pairs: []struct { + a, b Match + expected int + }{ + {m0, m1, 1}, // package-b > package-a + {m1, m0, -1}, // package-a < package-b + {m1, m2, 0}, // both package-a + }, + }, + { + name: "compareByPackageVersion", + compareFunc: compareByPackageVersion, + pairs: []struct { + a, b Match + expected int + }{ + {m1, m2, 1}, // 2.0.0 > 1.0.0 + {m2, m1, -1}, // 1.0.0 < 2.0.0 + {m2, m4, 0}, // both 1.0.0 + }, + }, + { + name: "compareByPackageType", + compareFunc: compareByPackageType, + pairs: []struct { + a, b Match + expected int + }{ + {m0, m1, 1}, // npm > docker + {m1, m0, -1}, // docker < npm + {m0, m2, 0}, // both npm + }, + }, + { + name: "compareByRisk", + compareFunc: compareByRisk, + pairs: []struct { + a, b Match + expected int + }{ + {m0, m1, -1}, // 75.0 > 50.0 + {m1, m0, 1}, // 50.0 < 75.0 + {m3, m4, -1}, // 10.0 > 5.0 + }, + }, + { + name: "compareByKEV", + compareFunc: compareByKEV, + pairs: []struct { + a, b Match + expected int + }{ + {m0, m2, 1}, // 0 < 1 KEV entry + {m2, m0, -1}, // 1 > 0 KEV entry + {m2, m4, 1}, // 1 < 2 KEV entries + {m4, m2, -1}, // 2 > 1 KEV entry + {m0, m1, 0}, // both 0 KEV entries + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, pair := range tt.pairs { + result := tt.compareFunc(pair.a, pair.b) + assert.Equal(t, pair.expected, result, "comparing %v and %v", pair.a.Vulnerability.ID, pair.b.Vulnerability.ID) + } + }) + } +} + +func TestCombinedCompareFunctions(t *testing.T) { + ms := createTestMatches() + m0 := ms[0] // medium severity, high risk, high EPSS, no KEV, package-b + m1 := ms[1] // critical severity, medium risk, medium EPSS, no KEV, package-a + m2 := ms[2] // high severity, low risk, low EPSS, has KEV, package-a + + t.Run("compareVulnerabilityAttributes", func(t *testing.T) { + result := compareVulnerabilityAttributes(m0, m1) + assert.Equal(t, -1, result, "CVE-2023-1111 should come before CVE-2023-2222") + + result = compareVulnerabilityAttributes(m1, m0) + assert.Equal(t, 1, result, "CVE-2023-2222 should come after CVE-2023-1111") }) - t.Run("SingleItem", func(t *testing.T) { - matches := []Match{ - { - Vulnerability: Vulnerability{ - VulnerabilityMetadata: VulnerabilityMetadata{ - ID: "CVE-2023-1111", - Severity: "medium", - }, - }, - Artifact: Package{ - Name: "package-a", - Version: "1.0.0", - }, - }, - } - expected := deepCopyMatches(matches) - // should not change anything with a single item - SortMatches(matches, SortByPackage) + t.Run("comparePackageAttributes", func(t *testing.T) { + result := comparePackageAttributes(m0, m1) + assert.Equal(t, 1, result, "package-b should come after package-a") - if diff := cmp.Diff(expected, matches); diff != "" { - t.Errorf("Single item mismatch (-want +got):\n%s", diff) - } + result = comparePackageAttributes(m1, m2) + assert.Equal(t, 1, result, "package-a 2.0.0 should come after package-a 1.0.0") + + result = comparePackageAttributes(m1, m1) + assert.Equal(t, 0, result, "same package should be equal") }) - t.Run("NilValues", func(t *testing.T) { - matches := []Match{ - { - Vulnerability: Vulnerability{ - VulnerabilityMetadata: VulnerabilityMetadata{ - ID: "CVE-2023-1111", - Severity: "", - }, - }, - Artifact: Package{ - Name: "", - Version: "", - }, - }, - { - Vulnerability: Vulnerability{ - VulnerabilityMetadata: VulnerabilityMetadata{ - ID: "CVE-2023-2222", - Severity: "low", - }, - }, - Artifact: Package{ - Name: "package-a", - Version: "1.0.0", - }, - }, - } + t.Run("combine function", func(t *testing.T) { + // create a combined function that first compares by severity, then by risk if severity is equal + combined := combine(compareBySeverity, compareByRisk) - expected := []Match{ - matches[0], // empty name comes first alphabetically - matches[1], // "package-a" - } + result := combined(m0, m1) + assert.Equal(t, 1, result, "medium should come after critical regardless of risk") - // should handle empty strings properly - SortMatches(matches, SortByPackage) + // create two matches with the same severity but different risk + m5 := m1 // critical severity, risk 50.0 + m6 := m1 + m6.Vulnerability.Risk = 60.0 // critical severity, risk 60.0 - if diff := cmp.Diff(expected, matches); diff != "" { - t.Errorf("Nil values mismatch (-want +got):\n%s", diff) - } + result = combined(m5, m6) + assert.Equal(t, 1, result, "with equal severity, lower risk (50.0) should come after higher risk (60.0)") + + result = combined(m6, m5) + assert.Equal(t, -1, result, "with equal severity, higher risk (60.0) should come before lower risk (50.0)") }) } +func TestSortWithStrategy(t *testing.T) { + matches := createTestMatches() + + // create a custom strategy that sorts only by vulnerability ID + customStrategy := sortStrategyImpl{compareByVulnerabilityID} + + expected := []Match{ + matches[0], // CVE-2023-1111 + matches[1], // CVE-2023-2222 + matches[2], // CVE-2023-3333 + matches[3], // CVE-2023-4444 + matches[4], // CVE-2023-5555 + } + + testMatches := deepCopyMatches(matches) + sortWithStrategy(testMatches, customStrategy) + + if diff := cmp.Diff(expected, testMatches); diff != "" { + t.Errorf("sortWithStrategy mismatch (-want +got):\n%s", diff) + } + + // create an empty strategy (should not change the order) + emptyStrategy := sortStrategyImpl{} + originalMatches := deepCopyMatches(matches) + sortWithStrategy(originalMatches, emptyStrategy) + + if diff := cmp.Diff(matches, originalMatches); diff != "" { + t.Errorf("Empty strategy should not change order (-original +after):\n%s", diff) + } +} + func deepCopyMatches(matches []Match) []Match { result := make([]Match, len(matches)) copy(result, matches) diff --git a/grype/presenter/models/source.go b/grype/presenter/models/source.go index 6bbb1fe6158..f21c0674f2f 100644 --- a/grype/presenter/models/source.go +++ b/grype/presenter/models/source.go @@ -15,9 +15,9 @@ type source struct { // newSource creates a new source object to be represented into JSON. func newSource(src syftSource.Description) (source, error) { switch m := src.Metadata.(type) { - case pkg.PURLFileMetadata: + case pkg.SBOMFileMetadata: return source{ - Type: "purl-file", + Type: "sbom-file", Target: m.Path, }, nil case pkg.PURLLiteralMetadata: diff --git a/grype/presenter/models/source_test.go b/grype/presenter/models/source_test.go index 325b772a4e8..851fdf9a076 100644 --- a/grype/presenter/models/source_test.go +++ b/grype/presenter/models/source_test.go @@ -65,12 +65,12 @@ func TestNewSource(t *testing.T) { { name: "purl-file", metadata: syftSource.Description{ - Metadata: pkg.PURLFileMetadata{ + Metadata: pkg.SBOMFileMetadata{ Path: "/path/to/purls.txt", }, }, expected: source{ - Type: "purl-file", + Type: "sbom-file", Target: "/path/to/purls.txt", }, }, diff --git a/grype/presenter/models/vulnerability.go b/grype/presenter/models/vulnerability.go index 391a9f636de..38216ce37cb 100644 --- a/grype/presenter/models/vulnerability.go +++ b/grype/presenter/models/vulnerability.go @@ -12,6 +12,7 @@ type Vulnerability struct { VulnerabilityMetadata Fix Fix `json:"fix"` Advisories []Advisory `json:"advisories"` + Risk float64 `json:"risk"` } type Fix struct { @@ -52,6 +53,7 @@ func NewVulnerability(vuln vulnerability.Vulnerability, metadata *vulnerability. State: string(vuln.Fix.State), }, Advisories: advisories, + Risk: metadata.RiskScore(), } } func sortVersions(fixedVersions []string, format version.Format) []string { diff --git a/grype/presenter/models/vulnerability_metadata.go b/grype/presenter/models/vulnerability_metadata.go index 98910f7955d..d02e6243227 100644 --- a/grype/presenter/models/vulnerability_metadata.go +++ b/grype/presenter/models/vulnerability_metadata.go @@ -71,9 +71,9 @@ func toKnownExploited(knownExploited []vulnerability.KnownExploited) []KnownExpl CVE: ke.CVE, VendorProject: ke.VendorProject, Product: ke.Product, - DateAdded: ke.DateAdded.Format(time.DateOnly), + DateAdded: formatDate(ke.DateAdded), RequiredAction: ke.RequiredAction, - DueDate: ke.DueDate.Format(time.DateOnly), + DueDate: formatDate(ke.DueDate), KnownRansomwareCampaignUse: ke.KnownRansomwareCampaignUse, Notes: ke.Notes, URLs: ke.URLs, @@ -83,6 +83,13 @@ func toKnownExploited(knownExploited []vulnerability.KnownExploited) []KnownExpl return result } +func formatDate(t *time.Time) string { + if t == nil { + return "" + } + return t.Format(time.DateOnly) +} + func toEPSS(epss []vulnerability.EPSS) []EPSS { result := make([]EPSS, len(epss)) for idx, e := range epss { diff --git a/grype/presenter/sarif/presenter.go b/grype/presenter/sarif/presenter.go index ac7cdfc2190..4bd6a3cc765 100644 --- a/grype/presenter/sarif/presenter.go +++ b/grype/presenter/sarif/presenter.go @@ -329,9 +329,23 @@ func securitySeverityValue(m models.Match) string { return "0.0" } +func levelValue(m models.Match) string { + severity := vulnerability.ParseSeverity(m.Vulnerability.Severity) + switch severity { + case vulnerability.CriticalSeverity: + return "error" + case vulnerability.HighSeverity: + return "error" + case vulnerability.MediumSeverity: + return "warning" + } + + return "note" +} + // subtitle generates a subtitle for the given match func subtitle(m models.Match) string { - subtitle := m.Vulnerability.VulnerabilityMetadata.Description + subtitle := m.Vulnerability.Description if subtitle != "" { return subtitle } @@ -360,6 +374,7 @@ func (p Presenter) sarifResults() []*sarif.Result { for _, m := range p.document.Matches { out = append(out, &sarif.Result{ RuleID: sp(p.ruleID(m)), + Level: sp(levelValue(m)), Message: p.resultMessage(m), // According to the SARIF spec, it may be correct to use AnalysisTarget.URI to indicate a logical // file such as a "Dockerfile" but GitHub does not work well with this @@ -392,8 +407,8 @@ func (p Presenter) resultMessage(m models.Match) sarif.Message { src = fmt.Sprintf("at: %s", path) case pkg.PURLLiteralMetadata: src = fmt.Sprintf("from purl literal %q", meta.PURL) - case pkg.PURLFileMetadata: - src = fmt.Sprintf("from purl file %s", meta.Path) + case pkg.SBOMFileMetadata: + src = fmt.Sprintf("from SBOM file %s", meta.Path) } message := fmt.Sprintf("A %s vulnerability in %s package: %s, version %s was found %s", severityText(m), m.Artifact.Type, m.Artifact.Name, m.Artifact.Version, src) diff --git a/grype/presenter/sarif/presenter_test.go b/grype/presenter/sarif/presenter_test.go index 50693d09da0..2b30cfec612 100644 --- a/grype/presenter/sarif/presenter_test.go +++ b/grype/presenter/sarif/presenter_test.go @@ -6,6 +6,7 @@ import ( "os/exec" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,7 +18,7 @@ import ( "github.com/anchore/syft/syft/source/directorysource" ) -var updateSnapshot = flag.Bool("update-sarif", false, "update .golden files for sarif presenters") +var updateSnapshot = flag.Bool("update", false, "update .golden files for sarif presenters") var validatorImage = "ghcr.io/anchore/sarif-validator:0.1.0@sha256:a0729d695e023740f5df6bcb50d134e88149bea59c63a896a204e88f62b564c6" func TestSarifPresenter(t *testing.T) { @@ -57,8 +58,8 @@ func TestSarifPresenter(t *testing.T) { actual = internal.Redact(actual) expected = internal.Redact(expected) - if !bytes.Equal(expected, actual) { - assert.JSONEq(t, string(expected), string(actual)) + if d := cmp.Diff(string(expected), string(actual)); d != "" { + t.Fatalf("(-want +got):\n%s", d) } }) } @@ -286,6 +287,7 @@ func TestToSarifReport(t *testing.T) { assert.Len(t, run.Results, 2) result := run.Results[0] assert.Equal(t, "CVE-1999-0001-package-1", *result.RuleID) + assert.Equal(t, "note", *result.Level) assert.Len(t, result.Locations, 1) location := result.Locations[0] expectedLocation, ok := tc.locations[*result.RuleID] @@ -296,6 +298,7 @@ func TestToSarifReport(t *testing.T) { result = run.Results[1] assert.Equal(t, "CVE-1999-0002-package-2", *result.RuleID) + assert.Equal(t, "error", *result.Level) assert.Len(t, result.Locations, 1) location = result.Locations[0] expectedLocation, ok = tc.locations[*result.RuleID] diff --git a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden index 9edb968a3a0..91900ee484b 100644 --- a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden +++ b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden @@ -16,15 +16,15 @@ "text": "CVE-1999-0001 low vulnerability for package-1 package" }, "fullDescription": { - "text": "1999-01 description" + "text": "Version 1.1.1 is affected with an available fix in versions 1.2.1,2.1.3,3.4.0" }, "helpUri": "https://github.com/anchore/grype", "help": { - "text": "Vulnerability CVE-1999-0001\nSeverity: low\nPackage: package-1\nVersion: 1.1.1\nFix Version: 1.2.1,2.1.3,3.4.0\nType: rpm\nLocation: /some/path/somefile-1.txt\nData Namespace: source-1\nLink: CVE-1999-0001", - "markdown": "**Vulnerability CVE-1999-0001**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| low | package-1 | 1.1.1 | 1.2.1,2.1.3,3.4.0 | rpm | /some/path/somefile-1.txt | source-1 | CVE-1999-0001 |\n" + "text": "Vulnerability CVE-1999-0001\nSeverity: low\nPackage: package-1\nVersion: 1.1.1\nFix Version: 1.2.1,2.1.3,3.4.0\nType: rpm\nLocation: /some/path/somefile-1.txt\nData Namespace: \nLink: CVE-1999-0001", + "markdown": "**Vulnerability CVE-1999-0001**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| low | package-1 | 1.1.1 | 1.2.1,2.1.3,3.4.0 | rpm | /some/path/somefile-1.txt | | CVE-1999-0001 |\n" }, "properties": { - "security-severity": "4.0" + "security-severity": "8.2" } }, { @@ -34,18 +34,18 @@ "text": "CVE-1999-0002 critical vulnerability for package-2 package" }, "fullDescription": { - "text": "1999-02 description" + "text": "Version 2.2.2 is affected with no fixes reported yet." }, "helpUri": "https://github.com/anchore/grype", "help": { - "text": "Vulnerability CVE-1999-0002\nSeverity: critical\nPackage: package-2\nVersion: 2.2.2\nFix Version: \nType: deb\nLocation: /some/path/somefile-2.txt\nData Namespace: source-2\nLink: CVE-1999-0002", - "markdown": "**Vulnerability CVE-1999-0002**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| critical | package-2 | 2.2.2 | | deb | /some/path/somefile-2.txt | source-2 | CVE-1999-0002 |\n" + "text": "Vulnerability CVE-1999-0002\nSeverity: critical\nPackage: package-2\nVersion: 2.2.2\nFix Version: \nType: deb\nLocation: /some/path/somefile-2.txt\nData Namespace: \nLink: CVE-1999-0002", + "markdown": "**Vulnerability CVE-1999-0002**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| critical | package-2 | 2.2.2 | | deb | /some/path/somefile-2.txt | | CVE-1999-0002 |\n" }, "properties": { "purls": [ "pkg:deb/package-2@2.2.2" ], - "security-severity": "1.0" + "security-severity": "8.5" } } ] @@ -54,6 +54,7 @@ "results": [ { "ruleId": "CVE-1999-0001-package-1", + "level": "note", "message": { "text": "A low vulnerability in rpm package: package-1, version 1.1.1 was found at: /some/path/somefile-1.txt" }, @@ -78,6 +79,7 @@ }, { "ruleId": "CVE-1999-0002-package-2", + "level": "error", "message": { "text": "A critical vulnerability in deb package: package-2, version 2.2.2 was found at: /some/path/somefile-2.txt" }, diff --git a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden index 027cd809648..b9d33518d2b 100644 --- a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden +++ b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden @@ -16,15 +16,15 @@ "text": "CVE-1999-0001 low vulnerability for package-1 package" }, "fullDescription": { - "text": "1999-01 description" + "text": "Version 1.1.1 is affected with an available fix in versions 1.2.1,2.1.3,3.4.0" }, "helpUri": "https://github.com/anchore/grype", "help": { - "text": "Vulnerability CVE-1999-0001\nSeverity: low\nPackage: package-1\nVersion: 1.1.1\nFix Version: 1.2.1,2.1.3,3.4.0\nType: rpm\nLocation: somefile-1.txt\nData Namespace: source-1\nLink: CVE-1999-0001", - "markdown": "**Vulnerability CVE-1999-0001**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| low | package-1 | 1.1.1 | 1.2.1,2.1.3,3.4.0 | rpm | somefile-1.txt | source-1 | CVE-1999-0001 |\n" + "text": "Vulnerability CVE-1999-0001\nSeverity: low\nPackage: package-1\nVersion: 1.1.1\nFix Version: 1.2.1,2.1.3,3.4.0\nType: rpm\nLocation: somefile-1.txt\nData Namespace: \nLink: CVE-1999-0001", + "markdown": "**Vulnerability CVE-1999-0001**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| low | package-1 | 1.1.1 | 1.2.1,2.1.3,3.4.0 | rpm | somefile-1.txt | | CVE-1999-0001 |\n" }, "properties": { - "security-severity": "4.0" + "security-severity": "8.2" } }, { @@ -34,18 +34,18 @@ "text": "CVE-1999-0002 critical vulnerability for package-2 package" }, "fullDescription": { - "text": "1999-02 description" + "text": "Version 2.2.2 is affected with no fixes reported yet." }, "helpUri": "https://github.com/anchore/grype", "help": { - "text": "Vulnerability CVE-1999-0002\nSeverity: critical\nPackage: package-2\nVersion: 2.2.2\nFix Version: \nType: deb\nLocation: somefile-2.txt\nData Namespace: source-2\nLink: CVE-1999-0002", - "markdown": "**Vulnerability CVE-1999-0002**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| critical | package-2 | 2.2.2 | | deb | somefile-2.txt | source-2 | CVE-1999-0002 |\n" + "text": "Vulnerability CVE-1999-0002\nSeverity: critical\nPackage: package-2\nVersion: 2.2.2\nFix Version: \nType: deb\nLocation: somefile-2.txt\nData Namespace: \nLink: CVE-1999-0002", + "markdown": "**Vulnerability CVE-1999-0002**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| critical | package-2 | 2.2.2 | | deb | somefile-2.txt | | CVE-1999-0002 |\n" }, "properties": { "purls": [ "pkg:deb/package-2@2.2.2" ], - "security-severity": "1.0" + "security-severity": "8.5" } } ] @@ -54,6 +54,7 @@ "results": [ { "ruleId": "CVE-1999-0001-package-1", + "level": "note", "message": { "text": "A low vulnerability in rpm package: package-1, version 1.1.1 was found in image user-input at: somefile-1.txt" }, @@ -84,6 +85,7 @@ }, { "ruleId": "CVE-1999-0002-package-2", + "level": "error", "message": { "text": "A critical vulnerability in deb package: package-2, version 2.2.2 was found in image user-input at: somefile-2.txt" }, diff --git a/grype/presenter/table/__snapshots__/presenter_test.snap b/grype/presenter/table/__snapshots__/presenter_test.snap index 73149c75fee..b3aa7fcbd1c 100755 --- a/grype/presenter/table/__snapshots__/presenter_test.snap +++ b/grype/presenter/table/__snapshots__/presenter_test.snap @@ -1,15 +1,15 @@ [TestTablePresenter/no_color - 1] -NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY -package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low -package-2 2.2.2 deb CVE-1999-0002 Critical +NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY EPSS% RISK +package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 42.00 1.7 +package-2 2.2.2 deb CVE-1999-0002 Critical 53.00 96.3 (kev) --- [TestTablePresenter/with_color - 1] -NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY -package-1 1.1.1 1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low -package-2 2.2.2 deb CVE-1999-0002 Critical +NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY EPSS% RISK +package-1 1.1.1 1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 42.00 1.7 +package-2 2.2.2 deb CVE-1999-0002 Critical 53.00 96.3 KEV --- @@ -19,18 +19,35 @@ No vulnerabilities found --- [TestHidesIgnoredMatches - 1] -NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY -package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low -package-2 2.2.2 deb CVE-1999-0002 Critical +NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY EPSS% RISK +package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 42.00 1.7 +package-2 2.2.2 deb CVE-1999-0002 Critical 53.00 96.3 (kev) --- [TestDisplaysIgnoredMatches - 1] -NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY -package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low -package-2 2.2.2 deb CVE-1999-0002 Critical -package-2 2.2.2 deb CVE-1999-0001 Low (suppressed) -package-2 2.2.2 deb CVE-1999-0002 Critical (suppressed) -package-2 2.2.2 deb CVE-1999-0004 Critical (suppressed by VEX) +NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY EPSS% RISK +package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 42.00 1.7 +package-2 2.2.2 deb CVE-1999-0002 Critical 53.00 96.3 (kev) +package-2 2.2.2 deb CVE-1999-0001 Low 42.00 1.7 (suppressed) +package-2 2.2.2 deb CVE-1999-0002 Critical 53.00 96.3 (kev, suppressed) +package-2 2.2.2 deb CVE-1999-0004 High 75.00 2.2 (suppressed by VEX) + +--- + +[TestDisplaysDistro - 1] +NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY EPSS% RISK +package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 42.00 1.7 (ubuntu:2.5) +package-2 2.2.2 deb CVE-1999-0002 Critical 53.00 96.3 (kev, ubuntu:3.5) + +--- + +[TestDisplaysIgnoredMatchesAndDistro - 1] +NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY EPSS% RISK +package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 42.00 1.7 (ubuntu:2.5) +package-2 2.2.2 deb CVE-1999-0002 Critical 53.00 96.3 (kev, ubuntu:3.5) +package-2 2.2.2 deb CVE-1999-0001 Low 42.00 1.7 (ubuntu:2.5, suppressed) +package-2 2.2.2 deb CVE-1999-0002 Critical 53.00 96.3 (kev, ubuntu:3.5, suppressed) +package-2 2.2.2 deb CVE-1999-0004 High 75.00 2.2 (suppressed by VEX) --- diff --git a/grype/presenter/table/presenter.go b/grype/presenter/table/presenter.go index 3e0671490e1..874a51a72c6 100644 --- a/grype/presenter/table/presenter.go +++ b/grype/presenter/table/presenter.go @@ -1,6 +1,7 @@ package table import ( + "fmt" "io" "strings" @@ -8,13 +9,14 @@ import ( "github.com/olekukonko/tablewriter" "github.com/scylladb/go-set/strset" + "github.com/anchore/grype/grype/db/v5/namespace/distro" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" ) const ( - appendSuppressed = " (suppressed)" - appendSuppressedVEX = " (suppressed by VEX)" + appendSuppressed = "suppressed" + appendSuppressedVEX = "suppressed by VEX" ) // Presenter is a generic struct for holding fields needed for reporting @@ -24,6 +26,14 @@ type Presenter struct { withColor bool recommendedFixStyle lipgloss.Style + kevStyle lipgloss.Style + criticalStyle lipgloss.Style + highStyle lipgloss.Style + mediumStyle lipgloss.Style + lowStyle lipgloss.Style + negligibleStyle lipgloss.Style + auxiliaryStyle lipgloss.Style + unknownStyle lipgloss.Style } type rows []row @@ -35,6 +45,25 @@ type row struct { PackageType string VulnerabilityID string Severity string + EPSS epss + Risk string + Annotation string +} + +type epss struct { + Score float64 + Percentile float64 +} + +func (e epss) String() string { + percentile := e.Percentile * 100 + switch { + case percentile == 0: + return " N/A" + case percentile < 0.1: + return "< 0.1%" + } + return fmt.Sprintf("%5.2f", percentile) } // NewPresenter is a *Presenter constructor @@ -42,13 +71,22 @@ func NewPresenter(pb models.PresenterConfig, showSuppressed bool) *Presenter { withColor := supportsColor() fixStyle := lipgloss.NewStyle().Border(lipgloss.Border{Left: "*"}, false, false, false, true) if withColor { - fixStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true).Underline(true) + fixStyle = lipgloss.NewStyle() } return &Presenter{ document: pb.Document, showSuppressed: showSuppressed, withColor: withColor, recommendedFixStyle: fixStyle, + negligibleStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), // dark gray + lowStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("36")), // cyan/teal + mediumStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("178")), // gold/amber + highStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("203")), // salmon/light red + criticalStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("198")).Bold(true), // bright pink + kevStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("198")).Reverse(true).Bold(true), // white on bright pink + //kevStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("198")), // bright pink + auxiliaryStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), // dark gray + unknownStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), // light blue } } @@ -62,7 +100,7 @@ func (p *Presenter) Present(output io.Writer) error { } table := tablewriter.NewWriter(output) - table.SetHeader([]string{"Name", "Installed", "Fixed-In", "Type", "Vulnerability", "Severity"}) + table.SetHeader([]string{"Name", "Installed", "Fixed-In", "Type", "Vulnerability", "Severity", "EPSS%", "Risk"}) table.SetAutoWrapText(false) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) @@ -76,21 +114,7 @@ func (p *Presenter) Present(output io.Writer) error { table.SetTablePadding(" ") table.SetNoWhiteSpace(true) - if p.withColor { - for _, row := range rs.Deduplicate() { - severityColor := getSeverityColor(row.Severity) - table.Rich(row.Columns(), []tablewriter.Colors{ - {}, // name - {}, // version - {}, // fix - {}, // package type - {}, // vulnerability ID - severityColor, // severity - }) - } - } else { - table.AppendBulk(rs.Render()) - } + table.AppendBulk(rs.Render()) table.Render() @@ -100,9 +124,22 @@ func (p *Presenter) Present(output io.Writer) error { func (p *Presenter) getRows(doc models.Document, showSuppressed bool) rows { var rs rows + multipleDistros := false + existingDistro := "" + for _, m := range doc.Matches { + if _, err := distro.FromString(m.Vulnerability.Namespace); err == nil { + if existingDistro == "" { + existingDistro = m.Vulnerability.Namespace + } else if existingDistro != m.Vulnerability.Namespace { + multipleDistros = true + break + } + } + } + // generate rows for matching vulnerabilities for _, m := range doc.Matches { - rs = append(rs, p.newRow(m, "")) + rs = append(rs, p.newRow(m, "", multipleDistros)) } // generate rows for suppressed vulnerabilities @@ -116,7 +153,7 @@ func (p *Presenter) getRows(doc models.Document, showSuppressed bool) rows { } } } - rs = append(rs, p.newRow(m.Match, msg)) + rs = append(rs, p.newRow(m.Match, msg, multipleDistros)) } } return rs @@ -126,10 +163,34 @@ func supportsColor() bool { return lipgloss.NewStyle().Foreground(lipgloss.Color("5")).Render("") != "" } -func (p *Presenter) newRow(m models.Match, severitySuffix string) row { - severity := m.Vulnerability.Severity - if severity != "" { - severity += severitySuffix +func (p *Presenter) newRow(m models.Match, extraAnnotation string, showDistro bool) row { + var annotations []string + + if showDistro { + if d, err := distro.FromString(m.Vulnerability.Namespace); err == nil { + annotations = append(annotations, p.auxiliaryStyle.Render(fmt.Sprintf("%s:%s", d.DistroType(), d.Version()))) + } + } + + if extraAnnotation != "" { + annotations = append(annotations, p.auxiliaryStyle.Render(extraAnnotation)) + } + + var kev, annotation string + if len(m.Vulnerability.KnownExploited) > 0 { + if p.withColor { + kev = p.kevStyle.Reverse(false).Render("") + p.kevStyle.Render("KEV") + p.kevStyle.Reverse(false).Render("") // ⚡❋◆◉፨⿻⨳✖• + } else { + annotations = append([]string{"kev"}, annotations...) + } + } + + if len(annotations) > 0 { + annotation = p.auxiliaryStyle.Render("(") + strings.Join(annotations, p.auxiliaryStyle.Render(", ")) + p.auxiliaryStyle.Render(")") + } + + if kev != "" { + annotation = kev + " " + annotation } return row{ @@ -138,11 +199,58 @@ func (p *Presenter) newRow(m models.Match, severitySuffix string) row { Fix: p.formatFix(m), PackageType: string(m.Artifact.Type), VulnerabilityID: m.Vulnerability.ID, - Severity: severity, + Severity: p.formatSeverity(m.Vulnerability.Severity), + EPSS: newEPSS(m.Vulnerability.EPSS), + Risk: p.formatRisk(m.Vulnerability.Risk), + Annotation: annotation, + } +} + +func newEPSS(es []models.EPSS) epss { + if len(es) == 0 { + return epss{} + } + return epss{ + Score: es[0].EPSS, + Percentile: es[0].Percentile, + } +} + +func (p *Presenter) formatSeverity(severity string) string { + var severityStyle *lipgloss.Style + switch strings.ToLower(severity) { + case "critical": + severityStyle = &p.criticalStyle + case "high": + severityStyle = &p.highStyle + case "medium": + severityStyle = &p.mediumStyle + case "low": + severityStyle = &p.lowStyle + case "negligible": + severityStyle = &p.negligibleStyle + } + + if severityStyle == nil { + severityStyle = &p.unknownStyle } + + return severityStyle.Render(severity) +} + +func (p *Presenter) formatRisk(risk float64) string { + // TODO: add color to risk? + switch { + case risk == 0: + return " N/A" + case risk < 0.1: + return "< 0.1" + } + return fmt.Sprintf("%5.1f", risk) } func (p *Presenter) formatFix(m models.Match) string { + // adjust the model fix state values for better presentation switch m.Vulnerability.Fix.State { case vulnerability.FixStateWontFix.String(): return "(won't fix)" @@ -150,6 +258,19 @@ func (p *Presenter) formatFix(m models.Match) string { return "" } + // do our best to summarize the fixed versions, de-epmhasize non-recommended versions + // also, since there is not a lot of screen real estate, we will truncate the list of fixed versions + // to ~30 characters (or so) to avoid wrapping. + return p.applyTruncation( + p.formatVersionsToDisplay( + m, + getRecommendedVersions(m), + ), + m.Vulnerability.Fix.Versions, + ) +} + +func getRecommendedVersions(m models.Match) *strset.Set { recommended := strset.New() for _, d := range m.MatchDetails { if d.Fix == nil { @@ -159,22 +280,72 @@ func (p *Presenter) formatFix(m models.Match) string { recommended.Add(d.Fix.SuggestedVersion) } } + return recommended +} - var vers []string +const maxVersionFieldLength = 30 + +func (p *Presenter) formatVersionsToDisplay(m models.Match, recommendedVersions *strset.Set) []string { hasMultipleVersions := len(m.Vulnerability.Fix.Versions) > 1 + shouldHighlightRecommended := hasMultipleVersions && recommendedVersions.Size() > 0 + + var currentCharacterCount int + added := strset.New() + var vers []string + for _, v := range m.Vulnerability.Fix.Versions { - if hasMultipleVersions && recommended.Has(v) { - vers = append(vers, p.recommendedFixStyle.Render(v)) - continue + if added.Has(v) { + continue // skip duplicates + } + + if shouldHighlightRecommended { + if recommendedVersions.Has(v) { + // recommended versions always get added + added.Add(v) + currentCharacterCount += len(v) + vers = append(vers, p.recommendedFixStyle.Render(v)) + continue + } + + // skip not-necessarily-recommended versions if we're running out of space + if currentCharacterCount+len(v) > maxVersionFieldLength { + continue + } + + // add not-necessarily-recommended versions with auxiliary styling + currentCharacterCount += len(v) + added.Add(v) + vers = append(vers, p.auxiliaryStyle.Render(v)) + } else { + // when not prioritizing, add all versions + added.Add(v) + vers = append(vers, v) } - vers = append(vers, v) } - return strings.Join(vers, ", ") + return vers +} + +func (p *Presenter) applyTruncation(formattedVersions []string, allVersions []string) string { + finalVersions := strings.Join(formattedVersions, p.auxiliaryStyle.Render(", ")) + + var characterCount int + for _, v := range allVersions { + characterCount += len(v) + } + + if characterCount > maxVersionFieldLength && len(allVersions) > 1 { + finalVersions += p.auxiliaryStyle.Render(", ...") + } + + return finalVersions } func (r row) Columns() []string { - return []string{r.Name, r.Version, r.Fix, r.PackageType, r.VulnerabilityID, r.Severity} + if r.Annotation != "" { + return []string{r.Name, r.Version, r.Fix, r.PackageType, r.VulnerabilityID, r.Severity, r.EPSS.String(), r.Risk, r.Annotation} + } + return []string{r.Name, r.Version, r.Fix, r.PackageType, r.VulnerabilityID, r.Severity, r.EPSS.String(), r.Risk} } func (r row) String() string { @@ -209,23 +380,3 @@ func (rs rows) Deduplicate() []row { // render final columns return deduped } - -func getSeverityColor(severity string) tablewriter.Colors { - severityFontType, severityColor := tablewriter.Normal, tablewriter.Normal - - switch strings.ToLower(severity) { - case "critical": - severityFontType = tablewriter.Bold - severityColor = tablewriter.FgRedColor - case "high": - severityColor = tablewriter.FgRedColor - case "medium": - severityColor = tablewriter.FgYellowColor - case "low": - severityColor = tablewriter.FgGreenColor - case "negligible": - severityColor = tablewriter.FgBlueColor - } - - return tablewriter.Colors{severityFontType, severityColor} -} diff --git a/grype/presenter/table/presenter_test.go b/grype/presenter/table/presenter_test.go index bdd93f97bde..24bd168611a 100644 --- a/grype/presenter/table/presenter_test.go +++ b/grype/presenter/table/presenter_test.go @@ -33,18 +33,26 @@ func TestCreateRow(t *testing.T) { Versions: []string{"1.0.2", "2.0.1", "3.0.4"}, State: vulnerability.FixStateFixed.String(), }, + Risk: 87.2, VulnerabilityMetadata: models.VulnerabilityMetadata{ ID: "CVE-1999-0001", Namespace: "source-1", Description: "1999-01 description", - Severity: "Low", + Severity: "Medium", Cvss: []models.Cvss{ { Metrics: models.CvssMetrics{ - BaseScore: 4, + BaseScore: 7, }, - Vector: "another vector", - Version: "3.0", + Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", + Version: "3.1", + }, + }, + EPSS: []models.EPSS{ + { + CVE: "CVE-1999-0001", + EPSS: 0.3, + Percentile: 0.5, }, }, }, @@ -60,30 +68,43 @@ func TestCreateRow(t *testing.T) { }, }, } + + matchWithKev := match1 + matchWithKev.Vulnerability.KnownExploited = append(matchWithKev.Vulnerability.KnownExploited, models.KnownExploited{ + CVE: "CVE-1999-0001", + KnownRansomwareCampaignUse: "Known", + }) + cases := []struct { - name string - match models.Match - severitySuffix string - expectedRow []string + name string + match models.Match + extraAnnotation string + expectedRow []string }{ { - name: "create row for vulnerability", - match: match1, - severitySuffix: "", - expectedRow: []string{match1.Artifact.Name, match1.Artifact.Version, "1.0.2, *2.0.1, 3.0.4", string(match1.Artifact.Type), match1.Vulnerability.ID, "Low"}, + name: "create row for vulnerability", + match: match1, + extraAnnotation: "", + expectedRow: []string{match1.Artifact.Name, match1.Artifact.Version, "1.0.2, *2.0.1, 3.0.4", string(match1.Artifact.Type), match1.Vulnerability.ID, "Medium", "50.00", " 87.2"}, }, { - name: "create row for suppressed vulnerability", - match: match1, - severitySuffix: appendSuppressed, - expectedRow: []string{match1.Artifact.Name, match1.Artifact.Version, "1.0.2, *2.0.1, 3.0.4", string(match1.Artifact.Type), match1.Vulnerability.ID, "Low (suppressed)"}, + name: "create row for suppressed vulnerability", + match: match1, + extraAnnotation: appendSuppressed, + expectedRow: []string{match1.Artifact.Name, match1.Artifact.Version, "1.0.2, *2.0.1, 3.0.4", string(match1.Artifact.Type), match1.Vulnerability.ID, "Medium", "50.00", " 87.2", "(suppressed)"}, + }, + { + name: "create row for suppressed vulnerability + Kev", + match: matchWithKev, + extraAnnotation: appendSuppressed, + expectedRow: []string{match1.Artifact.Name, match1.Artifact.Version, "1.0.2, *2.0.1, 3.0.4", string(match1.Artifact.Type), match1.Vulnerability.ID, "Medium", "50.00", " 87.2", "(kev, suppressed)"}, }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { p := NewPresenter(models.PresenterConfig{}, false) - row := p.newRow(testCase.match, testCase.severitySuffix) + row := p.newRow(testCase.match, testCase.extraAnnotation, false) cols := rows{row}.Render()[0] assert.Equal(t, testCase.expectedRow, cols) @@ -175,6 +196,45 @@ func TestDisplaysIgnoredMatches(t *testing.T) { snaps.MatchSnapshot(t, actual) } +func TestDisplaysDistro(t *testing.T) { + var buffer bytes.Buffer + pb := models.PresenterConfig{ + Document: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource), + } + + pb.Document.Matches[0].Vulnerability.Namespace = "ubuntu:distro:ubuntu:2.5" + pb.Document.Matches[1].Vulnerability.Namespace = "ubuntu:distro:ubuntu:3.5" + + pres := NewPresenter(pb, false) + + err := pres.Present(&buffer) + require.NoError(t, err) + + actual := buffer.String() + snaps.MatchSnapshot(t, actual) +} + +func TestDisplaysIgnoredMatchesAndDistro(t *testing.T) { + var buffer bytes.Buffer + pb := models.PresenterConfig{ + Document: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource), + } + + pb.Document.Matches[0].Vulnerability.Namespace = "ubuntu:distro:ubuntu:2.5" + pb.Document.Matches[1].Vulnerability.Namespace = "ubuntu:distro:ubuntu:3.5" + + pb.Document.IgnoredMatches[0].Vulnerability.Namespace = "ubuntu:distro:ubuntu:2.5" + pb.Document.IgnoredMatches[1].Vulnerability.Namespace = "ubuntu:distro:ubuntu:3.5" + + pres := NewPresenter(pb, true) + + err := pres.Present(&buffer) + require.NoError(t, err) + + actual := buffer.String() + snaps.MatchSnapshot(t, actual) +} + func TestRowsRender(t *testing.T) { t.Run("empty rows returns empty slice", func(t *testing.T) { @@ -191,7 +251,7 @@ func TestRowsRender(t *testing.T) { result := rs.Render() expected := [][]string{ - {"pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical"}, + {"pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", "75.00", " N/A"}, } if diff := cmp.Diff(expected, result); diff != "" { @@ -209,9 +269,9 @@ func TestRowsRender(t *testing.T) { result := rs.Render() expected := [][]string{ - {"pkgA", "1.0.0", "", "os", "CVE-2023-1234", "critical"}, - {"pkgB", "2.0.0", "(won't fix)", "os", "CVE-2023-5678", "high"}, - {"pkgC", "3.0.0", "3.1.0", "os", "CVE-2023-9012", "medium"}, + {"pkgA", "1.0.0", "", "os", "CVE-2023-1234", "critical", "75.00", " N/A"}, + {"pkgB", "2.0.0", "(won't fix)", "os", "CVE-2023-5678", "high", "75.00", " N/A"}, + {"pkgC", "3.0.0", "3.1.0", "os", "CVE-2023-9012", "medium", "75.00", " N/A"}, } if diff := cmp.Diff(expected, result); diff != "" { @@ -226,17 +286,15 @@ func TestRowsRender(t *testing.T) { result := rs.Render() expected := [][]string{ - {"pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical"}, + {"pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", "75.00", " N/A"}, } if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("Render() mismatch (-want +got):\n%s", diff) } - // should have 7 columns: name, version, fix, packageType, vulnID, severity - if len(result[0]) != 6 { - t.Errorf("Expected 7 columns, got %d", len(result[0])) - } + // expected columns: name, version, fix, packageType, vulnID, severity, epss, risk + assert.Len(t, result[0], 8) }) } @@ -251,6 +309,24 @@ func createTestRow(name, version, fix, pkgType, vulnID, severity string, fixStat VulnerabilityMetadata: models.VulnerabilityMetadata{ ID: vulnID, Severity: severity, + Cvss: []models.Cvss{ + { + Source: "nvd", + Type: "CVSS", + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:L", + Metrics: models.CvssMetrics{ + BaseScore: 7.2, + }, + }, + }, + EPSS: []models.EPSS{ + { + CVE: vulnID, + EPSS: 0.03, + Percentile: 0.75, + }, + }, }, }, Artifact: models.Package{ @@ -261,7 +337,7 @@ func createTestRow(name, version, fix, pkgType, vulnID, severity string, fixStat } p := NewPresenter(models.PresenterConfig{}, false) - r := p.newRow(m, "") + r := p.newRow(m, "", false) return r, nil } diff --git a/grype/presenter/template/presenter.go b/grype/presenter/template/presenter.go index bf61c055b80..402b92017c5 100644 --- a/grype/presenter/template/presenter.go +++ b/grype/presenter/template/presenter.go @@ -8,9 +8,9 @@ import ( "text/template" "github.com/Masterminds/sprig/v3" - "github.com/mitchellh/go-homedir" "github.com/anchore/clio" + "github.com/anchore/go-homedir" "github.com/anchore/grype/grype/presenter/models" ) diff --git a/grype/search/distro.go b/grype/search/distro.go index da5ffa809d0..65f526bf20e 100644 --- a/grype/search/distro.go +++ b/grype/search/distro.go @@ -24,7 +24,7 @@ type DistroCriteria struct { func (c *DistroCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) { ns, err := namespace.FromString(value.Namespace) if err != nil { - return false, fmt.Sprintf("unable to determine namespace for vulnerability %v: %v", value.Reference.ID, err), nil + return false, fmt.Sprintf("unable to determine namespace for vulnerability %v: %v", value.ID, err), nil } dns, ok := ns.(*distroNs.Namespace) if !ok || dns == nil { @@ -63,13 +63,13 @@ func matchesDistro(d *distro.Distro, ns *distroNs.Namespace) bool { return false } - ty := namespace.DistroTypeString(d.Type) - - distroType := ns.DistroType() - if distroType != d.Type && distroType != distro.Type(ty) { + distroType := mimicV6DistroTypeOverrides(ns.DistroType()) + targetType := mimicV6DistroTypeOverrides(d.Type) + if distroType != targetType { return false } - return compatibleVersion(d.FullVersion(), ns.Version()) + + return compatibleVersion(d.Version, ns.Version()) } // compatibleVersion returns true when the versions are the same or the partial version describes the matching parts @@ -86,3 +86,34 @@ func compatibleVersion(fullVersion string, partialVersion string) bool { } return false } + +// TODO: this is a temporary workaround... in the long term the mock should more strongly enforce +// data overrides and not require this kind of logic being baked into mocks directly. +func mimicV6DistroTypeOverrides(t distro.Type) distro.Type { + overrideMap := map[string]string{ + "centos": "rhel", + "rocky": "rhel", + "rockylinux": "rhel", + "alma": "rhel", + "almalinux": "rhel", + "gentoo": "rhel", + "archlinux": "arch", + "oracle": "ol", + "oraclelinux": "ol", + "amazon": "amzn", + "amazonlinux": "amzn", + } + + applyMapping := func(i string) distro.Type { + if replacement, exists := distro.IDMapping[i]; exists { + return replacement + } + return distro.Type(i) + } + + if replacement, exists := overrideMap[string(t)]; exists { + return applyMapping(replacement) + } + + return applyMapping(string(t)) +} diff --git a/grype/search/ecosystem.go b/grype/search/ecosystem.go index 6ce6954166d..17aa8e8df91 100644 --- a/grype/search/ecosystem.go +++ b/grype/search/ecosystem.go @@ -25,7 +25,7 @@ type EcosystemCriteria struct { func (c *EcosystemCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) { ns, err := namespace.FromString(value.Namespace) if err != nil { - return false, fmt.Sprintf("unable to determine namespace for vulnerability %v: %v", value.Reference.ID, err), nil + return false, fmt.Sprintf("unable to determine namespace for vulnerability %v: %v", value.ID, err), nil } lang, ok := ns.(*language.Namespace) if !ok || lang == nil { diff --git a/grype/version/constraint_expression.go b/grype/version/constraint_expression.go index 9870863b0f6..6fc97cc7865 100644 --- a/grype/version/constraint_expression.go +++ b/grype/version/constraint_expression.go @@ -7,6 +7,8 @@ import ( "text/scanner" ) +var ErrFallbackToFuzzy = fmt.Errorf("falling back to fuzzy version matching") + type constraintExpression struct { units [][]constraintUnit // only supports or'ing a group of and'ed groups comparators [][]Comparator // only supports or'ing a group of and'ed groups @@ -20,7 +22,7 @@ func newConstraintExpression(phrase string, genFn comparatorGenerator) (constrai orUnits := make([][]constraintUnit, len(orParts)) orComparators := make([][]Comparator, len(orParts)) - + var fuzzyErr error for orIdx, andParts := range orParts { andUnits := make([]constraintUnit, len(andParts)) andComparators := make([]Comparator, len(andParts)) @@ -36,7 +38,16 @@ func newConstraintExpression(phrase string, genFn comparatorGenerator) (constrai comparator, err := genFn(*unit) if err != nil { - return constraintExpression{}, fmt.Errorf("failed to create comparator for '%s': %w", unit, err) + // this is a version constraint that could not be parsed as its + // specified type. Try falling back to fuzzy matching so that + // a match can still be attempted. + comparator, err = newFuzzyComparator(*unit) + if err != nil { + return constraintExpression{}, fmt.Errorf("failed to create comparator for '%s': %w", unit, err) + } + // Tell the caller we had to fallback from the specified + // version constraint format + fuzzyErr = ErrFallbackToFuzzy } andComparators[andIdx] = comparator } @@ -48,7 +59,7 @@ func newConstraintExpression(phrase string, genFn comparatorGenerator) (constrai return constraintExpression{ units: orUnits, comparators: orComparators, - }, nil + }, fuzzyErr } func (c *constraintExpression) satisfied(other *Version) (bool, error) { diff --git a/grype/version/constraint_expression_test.go b/grype/version/constraint_expression_test.go index 664fa465443..4f5e8ba40f6 100644 --- a/grype/version/constraint_expression_test.go +++ b/grype/version/constraint_expression_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/go-test/deep" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" ) func TestScanExpression(t *testing.T) { @@ -86,3 +88,91 @@ func TestScanExpression(t *testing.T) { }) } } + +func TestNewConstraintExpression(t *testing.T) { + tests := []struct { + name string + phrase string + genFn comparatorGenerator + expected constraintExpression + wantErr error + }{ + { + name: "single valid constraint", + phrase: "<1.1.1", + genFn: newGolangComparator, + expected: constraintExpression{ + units: [][]constraintUnit{ + {constraintUnit{ + rangeOperator: LT, + version: "1.1.1", + }}, + }, + comparators: [][]Comparator{ + {mustGolangComparator(t, constraintUnit{ + rangeOperator: LT, + version: "1.1.1", + })}, + }, + }, + wantErr: nil, + }, + { + name: "fall back to fuzzy on invalid semver", + phrase: ">9.6.0b1", + genFn: newGolangComparator, + expected: constraintExpression{ + units: [][]constraintUnit{ + {constraintUnit{ + rangeOperator: GT, + version: "9.6.0b1", + }}, + }, + comparators: [][]Comparator{ + {mustFuzzyComparator(t, constraintUnit{ + rangeOperator: GT, + version: "9.6.0b1", + })}, + }, + }, + wantErr: ErrFallbackToFuzzy, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := newConstraintExpression(test.phrase, test.genFn) + if test.wantErr != nil { + require.ErrorIs(t, err, test.wantErr) + } else { + require.NoError(t, err) + } + + opts := []cmp.Option{ + cmp.AllowUnexported(constraintExpression{}, + constraintUnit{}, golangVersion{}, fuzzyVersion{}, semanticVersion{}), + } + if diff := cmp.Diff(test.expected, actual, opts...); diff != "" { + t.Errorf("actual does not match expected, diff: %s", diff) + } + }) + } +} + +func mustGolangComparator(t *testing.T, unit constraintUnit) Comparator { + t.Helper() + c, err := newGolangComparator(unit) + if err != nil { + t.Fatal(err) + } + return c +} + +func mustFuzzyComparator(t *testing.T, unit constraintUnit) Comparator { + t.Helper() + c, err := newFuzzyComparator(unit) + if err != nil { + t.Fatal(err) + } + return c +} diff --git a/grype/version/fuzzy_constraint.go b/grype/version/fuzzy_constraint.go index fd68b88fa5d..8cf0ac32b77 100644 --- a/grype/version/fuzzy_constraint.go +++ b/grype/version/fuzzy_constraint.go @@ -176,6 +176,8 @@ func parseVersionParts(v string) (int, int, int) { // !"#$%&'()*+,-./ are dec 33 to 47, :;<=>?@ are dec 58 to 64, [\]^_` are dec 91 to 96 and {|}~ are dec 123 to 126. // So, punctuation is in dec 33-126 range except 48-57, 65-90 and 97-122 gaps. // This inverse logic allows for early short-circuiting for most of the chars and shaves ~20ns in benchmarks. + // linters might yell about De Morgan's law here - we ignore them in this case + //nolint:staticcheck return b >= '!' && b <= '~' && !(b > '/' && b < ':' || b > '@' && b < '[' || diff --git a/grype/version/pep440_constraint.go b/grype/version/pep440_constraint.go index cc0c0b319ea..4ea4c479355 100644 --- a/grype/version/pep440_constraint.go +++ b/grype/version/pep440_constraint.go @@ -44,7 +44,7 @@ func newPep440Constraint(raw string) (pep440Constraint, error) { constraints, err := newConstraintExpression(raw, newPep440Comparator) if err != nil { - return pep440Constraint{}, fmt.Errorf("unable to parse pep440 constrain phrase %w", err) + return pep440Constraint{}, fmt.Errorf("unable to parse pep440 constraint phrase %w", err) } return pep440Constraint{ diff --git a/grype/vulnerability/metadata.go b/grype/vulnerability/metadata.go index 633978e5229..5351f2baa01 100644 --- a/grype/vulnerability/metadata.go +++ b/grype/vulnerability/metadata.go @@ -1,19 +1,125 @@ package vulnerability import ( + "strings" "time" ) type Metadata struct { ID string - DataSource string + DataSource string // the primary reference URL, i.e. where the data originated Namespace string Severity string - URLs []string + URLs []string // secondary reference URLs a vulnerability may provide Description string Cvss []Cvss KnownExploited []KnownExploited EPSS []EPSS + + // calculated as-needed + risk float64 +} + +// RiskScore computes a basic quantitative risk by combining threat and severity. +// Threat is represented by epss (likelihood of exploitation), and severity by the cvss base score + string severity. +// Impact is currently fixed at 1 and may be integrated into the calculation in future versions. +// Raw risk is epss * (cvss / 10) * impact, then scaled to 0–100 for readability. +// If a vulnerability appears in the KEV list, apply an additional boost to reflect known exploitation. +// Known ransomware campaigns receive a further, distinct boost. +func (m *Metadata) RiskScore() float64 { + if m == nil { + return 0 + } + if m.risk != 0 { + return m.risk + } + m.risk = riskScore(*m) + return m.risk +} + +func riskScore(m Metadata) float64 { + return min(threat(m)*severity(m)*kevModifier(m), 1.0) * 100.0 +} + +func kevModifier(m Metadata) float64 { + if len(m.KnownExploited) > 0 { + for _, kev := range m.KnownExploited { + if strings.ToLower(kev.KnownRansomwareCampaignUse) == "known" { + // consider ransomware campaigns to be a greater kevModifier than other KEV threats + return 1.1 + } + } + return 1.05 // boost the final result, as if there is a greater kevModifier inherently from KEV threats + } + return 1.0 +} + +func threat(m Metadata) float64 { + if len(m.KnownExploited) > 0 { + // per the EPSS guidance, any evidence of exploitation in the wild (not just PoC) should be considered over EPSS data + return 1.0 + } + if len(m.EPSS) == 0 { + return 0.0 + } + return m.EPSS[0].EPSS +} + +// severity returns a 0-1 value, which is a combination of the string severity and the average of the cvss base scores. +// If there are no cvss scores, the string severity is used. Some vendors only update the string severity and not the +// cvss scores, so it's important to consider all sources. We are also not biasing towards any one source (multiple +// cvss scores won't over-weigh the string severity). +func severity(m Metadata) float64 { + // TODO: summarization should take a policy: prefer NVD over CNA or vice versa... + + stringSeverityScore := severityToScore(m.Severity) / 10.0 + avgBaseScore := average(validBaseScores(m.Cvss...)...) / 10.0 + if avgBaseScore == 0 { + return stringSeverityScore + } + return average(stringSeverityScore, avgBaseScore) +} + +func severityToScore(severity string) float64 { + // use the middle of the range for each severity + switch strings.ToLower(severity) { + case "negligible": + return 0.5 + case "low": + return 3.0 + case "medium": + return 5.0 + case "high": + return 7.5 + case "critical": + return 9.0 + } + // the severity value might be "unknown" or an unexpected value. These should not be lost + // in the noise and placed at the bottom of the list... instead we compromise to the middle of the list. + return 5.0 +} + +func validBaseScores(as ...Cvss) []float64 { + var out []float64 + for _, a := range as { + if a.Metrics.BaseScore == 0 { + // this is a mistake... base scores cannot be 0. Don't include this value and bring down the average + continue + } + out = append(out, a.Metrics.BaseScore) + } + return out +} + +func average(as ...float64) float64 { + if len(as) == 0 { + return 0 + } + sum := 0.0 + for _, a := range as { + sum += a + } + return sum / float64(len(as)) } type Cvss struct { diff --git a/grype/vulnerability/metadata_test.go b/grype/vulnerability/metadata_test.go new file mode 100644 index 00000000000..befcaa45a80 --- /dev/null +++ b/grype/vulnerability/metadata_test.go @@ -0,0 +1,457 @@ +package vulnerability + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRiskScore(t *testing.T) { + tests := []struct { + name string + metadata Metadata + expected float64 + }{ + { + name: "nil metadata", + metadata: Metadata{}, + expected: 0, + }, + { + name: "already calculated risk", + metadata: Metadata{ + risk: 42.5, + }, + expected: 42.5, + }, + { + name: "no EPSS data, no KEV", + metadata: Metadata{ + Severity: "high", + Cvss: []Cvss{ + { + Metrics: CvssMetrics{ + BaseScore: 7.5, + }, + }, + }, + }, + expected: 0, // threat is 0 without EPSS or KEV + }, + { + name: "with EPSS data, no KEV", + metadata: Metadata{ + Severity: "high", + EPSS: []EPSS{ + { + EPSS: 0.5, + Percentile: 0.95, + }, + }, + Cvss: []Cvss{ + { + Metrics: CvssMetrics{ + BaseScore: 7.5, + }, + }, + }, + }, + expected: 37.5, // 0.5 * (7.5/10) * 1 * 100 + }, + { + name: "with KEV, no EPSS", + metadata: Metadata{ + Severity: "high", + KnownExploited: []KnownExploited{ + { + CVE: "CVE-2023-1234", + KnownRansomwareCampaignUse: "No", + }, + }, + Cvss: []Cvss{ + { + Metrics: CvssMetrics{ + BaseScore: 7.5, + }, + }, + }, + }, + expected: 78.75, // 1.0 * (7.5/10) * 1.05* 100 + }, + { + name: "with KEV ransomware", + metadata: Metadata{ + Severity: "high", + KnownExploited: []KnownExploited{ + { + CVE: "CVE-2023-1234", + KnownRansomwareCampaignUse: "Known", + }, + }, + Cvss: []Cvss{ + { + Metrics: CvssMetrics{ + BaseScore: 7.5, + }, + }, + }, + }, + expected: 82.5, // 1.0 * (7.5/10) * 1.1 * 100 + }, + { + name: "with severity string only", + metadata: Metadata{ + Severity: "critical", + EPSS: []EPSS{ + { + EPSS: 0.8, + Percentile: 0.99, + }, + }, + }, + expected: 72, // 0.8 * (9.0/10) * 1.0 * 100 + }, + { + name: "with multiple CVSS scores + string severity", + metadata: Metadata{ + Severity: "medium", + EPSS: []EPSS{ + { + EPSS: 0.6, + Percentile: 0.90, + }, + }, + Cvss: []Cvss{ + { + Source: "NVD", + Metrics: CvssMetrics{ + BaseScore: 6.5, + }, + }, + { + Source: "Vendor", + Metrics: CvssMetrics{ + BaseScore: 5.5, + }, + }, + }, + }, + expected: 33, // 0.6 * ( (((6.5+5.5)/2)+5)/2 /10) * 1.0 * 100 + }, + { + name: "with some invalid CVSS scores + string severity", + metadata: Metadata{ + Severity: "medium", + EPSS: []EPSS{ + { + EPSS: 0.4, + Percentile: 0.85, + }, + }, + Cvss: []Cvss{ + { + Source: "NVD", + Metrics: CvssMetrics{ + BaseScore: 0, // invalid, should be ignored + }, + }, + { + Source: "Vendor", + Metrics: CvssMetrics{ + BaseScore: 6.0, + }, + }, + }, + }, + expected: 22, // 0.4 * ((6.0+5)/2 /10) * 1.0 * 100 + }, + { + name: "unknown severity", + metadata: Metadata{ + Severity: "unknown", + EPSS: []EPSS{ + { + EPSS: 0.3, + Percentile: 0.80, + }, + }, + }, + expected: 15, // 0.3 * (5.0/10) * 1.0 * 100 + }, + { + name: "maximum risk clamp", + metadata: Metadata{ + Severity: "critical", + KnownExploited: []KnownExploited{ + { + CVE: "CVE-2023-1234", + KnownRansomwareCampaignUse: "Known", + }, + }, + Cvss: []Cvss{ + { + Metrics: CvssMetrics{ + BaseScore: 10.0, + }, + }, + }, + }, + expected: 100, // clamped to 100 as it would be 1.0 * 1.0 * 1.1 * 100 = 120 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.metadata.RiskScore() + assert.InDelta(t, tt.expected, result, 0.01, "RiskScore method returned incorrect value") + + // test the calculated value is cached + if tt.name != "already calculated risk" && tt.name != "nil metadata" { + require.InDelta(t, tt.expected, tt.metadata.risk, 0.01, "risk was not cached") + } + + // test the standalone function + if tt.name != "nil metadata" && tt.name != "already calculated risk" { + funcResult := riskScore(tt.metadata) + assert.InDelta(t, tt.expected, funcResult, 0.0001, "riskScore function returned incorrect value") + } + }) + } +} + +func TestSeverityToScore(t *testing.T) { + tests := []struct { + severity string + expected float64 + }{ + {"negligible", 0.5}, + {"NEGLIGIBLE", 0.5}, + {"low", 3.0}, + {"LOW", 3.0}, + {"medium", 5.0}, + {"MEDIUM", 5.0}, + {"high", 7.5}, + {"HIGH", 7.5}, + {"critical", 9.0}, + {"CRITICAL", 9.0}, + {"unknown", 5.0}, + {"", 5.0}, + {"something-else", 5.0}, + } + + for _, tt := range tests { + t.Run(tt.severity, func(t *testing.T) { + result := severityToScore(tt.severity) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestAverageCVSS(t *testing.T) { + tests := []struct { + name string + cvss []Cvss + expected float64 + }{ + { + name: "empty slice", + cvss: []Cvss{}, + expected: 0, + }, + { + name: "single valid score", + cvss: []Cvss{ + {Metrics: CvssMetrics{BaseScore: 7.5}}, + }, + expected: 7.5, + }, + { + name: "multiple valid scores", + cvss: []Cvss{ + {Metrics: CvssMetrics{BaseScore: 7.5}}, + {Metrics: CvssMetrics{BaseScore: 8.5}}, + {Metrics: CvssMetrics{BaseScore: 9.0}}, + }, + expected: 8.33333, + }, + { + name: "with invalid scores", + cvss: []Cvss{ + {Metrics: CvssMetrics{BaseScore: 0}}, // invalid + {Metrics: CvssMetrics{BaseScore: 7.5}}, + {Metrics: CvssMetrics{BaseScore: 0}}, // invalid + {Metrics: CvssMetrics{BaseScore: 8.5}}, + }, + expected: 8.0, + }, + { + name: "all invalid scores", + cvss: []Cvss{ + {Metrics: CvssMetrics{BaseScore: 0}}, + {Metrics: CvssMetrics{BaseScore: 0}}, + }, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := average(validBaseScores(tt.cvss...)...) + assert.InDelta(t, tt.expected, result, 0.00001) + }) + } +} + +func TestThreat(t *testing.T) { + tests := []struct { + name string + metadata Metadata + expected float64 + }{ + { + name: "no EPSS, no KEV", + metadata: Metadata{}, + expected: 0, + }, + { + name: "with EPSS, no KEV", + metadata: Metadata{ + EPSS: []EPSS{ + {EPSS: 0.75}, + }, + }, + expected: 0.75, + }, + { + name: "with KEV, no EPSS", + metadata: Metadata{ + KnownExploited: []KnownExploited{ + {CVE: "CVE-2023-1234"}, + }, + }, + expected: 1.0, + }, + { + name: "with KEV and EPSS", + metadata: Metadata{ + EPSS: []EPSS{ + {EPSS: 0.5}, + }, + KnownExploited: []KnownExploited{ + {CVE: "CVE-2023-1234"}, + }, + }, + expected: 1.0, // KEV takes precedence + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := threat(tt.metadata) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestImpact(t *testing.T) { + tests := []struct { + name string + metadata Metadata + expected float64 + }{ + { + name: "no KEV", + metadata: Metadata{}, + expected: 1.0, + }, + { + name: "KEV without ransomware", + metadata: Metadata{ + KnownExploited: []KnownExploited{ + {KnownRansomwareCampaignUse: "No"}, + }, + }, + expected: 1.05, + }, + { + name: "KEV with ransomware", + metadata: Metadata{ + KnownExploited: []KnownExploited{ + {KnownRansomwareCampaignUse: "Known"}, + }, + }, + expected: 1.1, + }, + { + name: "KEV with case insensitive ransomware", + metadata: Metadata{ + KnownExploited: []KnownExploited{ + {KnownRansomwareCampaignUse: "KNOWN"}, + }, + }, + expected: 1.1, + }, + { + name: "multiple KEV entries, one with ransomware", + metadata: Metadata{ + KnownExploited: []KnownExploited{ + {KnownRansomwareCampaignUse: "No"}, + {KnownRansomwareCampaignUse: "Known"}, + }, + }, + expected: 1.1, // highest wins + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := kevModifier(tt.metadata) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestSeverity(t *testing.T) { + tests := []struct { + name string + metadata Metadata + expected float64 + }{ + { + name: "no CVSS, medium severity", + metadata: Metadata{ + Severity: "medium", + }, + expected: 0.5, + }, + { + name: "with CVSS + severity string", + metadata: Metadata{ + Severity: "medium", + Cvss: []Cvss{ + {Metrics: CvssMetrics{BaseScore: 8.0}}, + }, + }, + expected: 0.65, + }, + { + name: "multiple CVSS scores + severity string", + metadata: Metadata{ + Severity: "medium", + Cvss: []Cvss{ + {Metrics: CvssMetrics{BaseScore: 6.0}}, + {Metrics: CvssMetrics{BaseScore: 8.0}}, + }, + }, + expected: 0.6, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := severity(tt.metadata) + assert.InDelta(t, tt.expected, result, 0.00001) + }) + } +} diff --git a/grype/vulnerability/mock/vulnerability_provider.go b/grype/vulnerability/mock/vulnerability_provider.go index 346300db910..276daa95798 100644 --- a/grype/vulnerability/mock/vulnerability_provider.go +++ b/grype/vulnerability/mock/vulnerability_provider.go @@ -31,10 +31,10 @@ func (s *mockProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vuln for _, vuln := range s.Vulnerabilities { if vuln.ID == ref.ID && vuln.Namespace == ref.Namespace { var meta *vulnerability.Metadata - if m, ok := vuln.Reference.Internal.(vulnerability.Metadata); ok { + if m, ok := vuln.Internal.(vulnerability.Metadata); ok { meta = &m } - if m, ok := vuln.Reference.Internal.(*vulnerability.Metadata); ok { + if m, ok := vuln.Internal.(*vulnerability.Metadata); ok { meta = m } if meta != nil { diff --git a/grype/vulnerability/provider.go b/grype/vulnerability/provider.go index 5e0743f2693..d395bcecd13 100644 --- a/grype/vulnerability/provider.go +++ b/grype/vulnerability/provider.go @@ -1,7 +1,9 @@ package vulnerability import ( + "encoding/json" "io" + "time" grypePkg "github.com/anchore/grype/grype/pkg" ) @@ -30,3 +32,63 @@ type Provider interface { io.Closer } + +type StoreMetadataProvider interface { + DataProvenance() (map[string]DataProvenance, error) +} + +type DataProvenance struct { + DateCaptured time.Time `json:"captured,omitempty"` + InputDigest string `json:"input,omitempty"` +} + +type ProviderStatus struct { + SchemaVersion string `json:"schemaVersion"` + From string `json:"from,omitempty"` + Built time.Time `json:"built,omitempty"` + Path string `json:"path,omitempty"` + Error error `json:"error,omitempty"` +} + +func (s ProviderStatus) MarshalJSON() ([]byte, error) { + errStr := "" + if s.Error != nil { + errStr = s.Error.Error() + } + + var t string + if !s.Built.IsZero() { + t = s.Built.Format(time.RFC3339) + } + + return json.Marshal(&struct { + SchemaVersion string `json:"schemaVersion"` + From string `json:"from,omitempty"` + Built string `json:"built,omitempty"` + Path string `json:"path,omitempty"` + Valid bool `json:"valid"` + Error string `json:"error,omitempty"` + }{ + SchemaVersion: s.SchemaVersion, + From: s.From, + Built: t, + Path: s.Path, + Valid: s.Error == nil, + Error: errStr, + }) +} + +func (s DataProvenance) MarshalJSON() ([]byte, error) { + var t string + if !s.DateCaptured.IsZero() { + t = s.DateCaptured.Format(time.RFC3339) + } + + return json.Marshal(&struct { + DateCaptured string `json:"captured,omitempty"` + InputDigest string `json:"input,omitempty"` + }{ + DateCaptured: t, + InputDigest: s.InputDigest, + }) +} diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go index 2ebc73ef7cb..0df6da3ca63 100644 --- a/grype/vulnerability_matcher.go +++ b/grype/vulnerability_matcher.go @@ -3,6 +3,7 @@ package grype import ( "errors" "fmt" + "runtime/debug" "strings" "github.com/wagoodman/go-partybus" @@ -19,7 +20,6 @@ import ( "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -64,7 +64,11 @@ func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Conte } }() - remainingMatches, ignoredMatches = m.findDBMatches(pkgs, context, progressMonitor) + remainingMatches, ignoredMatches, err = m.findDBMatches(pkgs, context, progressMonitor) + if err != nil { + err = fmt.Errorf("unable to find matches against vulnerability database: %w", err) + return remainingMatches, ignoredMatches, err + } remainingMatches, ignoredMatches, err = m.findVEXMatches(context, remainingMatches, ignoredMatches, progressMonitor) if err != nil { @@ -84,13 +88,18 @@ func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Conte return remainingMatches, ignoredMatches, nil } -func (m *VulnerabilityMatcher) findDBMatches(pkgs []pkg.Package, context pkg.Context, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch) { +func (m *VulnerabilityMatcher) findDBMatches(pkgs []pkg.Package, context pkg.Context, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) { var ignoredMatches []match.IgnoredMatch log.Trace("finding matches against DB") matches, err := m.searchDBForMatches(context.Distro, pkgs, progressMonitor) if err != nil { - // errors returned from matchers during searchDBForMatches were being logged and not returned, so just log them here + if match.IsFatalError(err) { + return nil, nil, err + } + + // other errors returned from matchers during searchDBForMatches were being + // logged and not returned, so just log them here log.WithFields("error", err).Debug("error(s) returned from searchDBForMatches") } @@ -111,7 +120,7 @@ func (m *VulnerabilityMatcher) findDBMatches(pkgs []pkg.Package, context pkg.Con ignoredMatches = m.mergeIgnoredMatches(originalIgnoredMatches, ignoredMatches) } - return &matches, ignoredMatches + return &matches, ignoredMatches, nil } func (m *VulnerabilityMatcher) mergeIgnoredMatches(allIgnoredMatches ...[]match.IgnoredMatch) []match.IgnoredMatch { @@ -129,31 +138,19 @@ func (m *VulnerabilityMatcher) mergeIgnoredMatches(allIgnoredMatches ...[]match. //nolint:funlen func (m *VulnerabilityMatcher) searchDBForMatches( - release *linux.Release, + d *distro.Distro, packages []pkg.Package, progressMonitor *monitorWriter, ) (match.Matches, error) { - var errs error var allMatches []match.Match var allIgnored []match.IgnoredMatch matcherIndex, defaultMatcher := newMatcherIndex(m.Matchers) - var d *distro.Distro - if release != nil { - d, errs = distro.NewFromRelease(*release) - if errs != nil { - log.Warnf("unable to determine linux distribution: %+v", errs) - errs = nil - } - if d != nil && d.Disabled() { - log.Warnf("unsupported linux distribution: %s", d.Name()) - return match.NewMatches(), nil - } - } - if defaultMatcher == nil { defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true}) } + + var matcherErrs []error for _, p := range packages { progressMonitor.PackagesProcessed.Increment() log.WithFields("package", displayPackage(p)).Trace("searching for vulnerability matches") @@ -169,10 +166,14 @@ func (m *VulnerabilityMatcher) searchDBForMatches( matchAgainst = []match.Matcher{defaultMatcher} } for _, theMatcher := range matchAgainst { - matches, ignoredMatches, err := theMatcher.Match(m.VulnerabilityProvider, p) + matches, ignoredMatches, err := callMatcherSafely(theMatcher, m.VulnerabilityProvider, p) if err != nil { + if match.IsFatalError(err) { + return match.Matches{}, err + } + log.WithFields("error", err, "package", displayPackage(p)).Warn("matcher returned error") - errs = errors.Join(errs, err) + matcherErrs = append(matcherErrs, err) } allIgnored = append(allIgnored, ignoredMatches...) @@ -206,7 +207,17 @@ func (m *VulnerabilityMatcher) searchDBForMatches( // update the total discovered matches after removing all duplicates and ignores progressMonitor.MatchesDiscovered.Set(int64(res.Count())) - return res, errs + return res, errors.Join(matcherErrs...) +} + +func callMatcherSafely(m match.Matcher, vp vulnerability.Provider, p pkg.Package) (matches []match.Match, ignoredMatches []match.IgnoredMatch, err error) { + // handle individual matcher panics + defer func() { + if e := recover(); e != nil { + err = match.NewFatalError(m.Type(), fmt.Errorf("%v at:\n%s", e, string(debug.Stack()))) + } + }() + return m.Match(vp, p) } func (m *VulnerabilityMatcher) findVEXMatches(context pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) { @@ -355,12 +366,12 @@ func ignoredMatchesDiff(subject []match.IgnoredMatch, other []match.IgnoredMatch otherMap := make(map[match.Fingerprint]struct{}) for _, a := range other { - otherMap[a.Match.Fingerprint()] = struct{}{} + otherMap[a.Fingerprint()] = struct{}{} } var diff []match.IgnoredMatch for _, b := range subject { - if _, ok := otherMap[b.Match.Fingerprint()]; !ok { + if _, ok := otherMap[b.Fingerprint()]; !ok { diff = append(diff, b) } } diff --git a/grype/vulnerability_matcher_test.go b/grype/vulnerability_matcher_test.go index 988b30ef9d6..b9a7010578e 100644 --- a/grype/vulnerability_matcher_test.go +++ b/grype/vulnerability_matcher_test.go @@ -1,6 +1,7 @@ package grype import ( + "errors" "testing" "github.com/google/go-cmp/cmp" @@ -17,6 +18,7 @@ import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/matcher/apk" + matcherMock "github.com/anchore/grype/grype/matcher/mock" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" @@ -27,7 +29,6 @@ import ( "github.com/anchore/grype/internal/bus" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -250,7 +251,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "no matches", fields: fields{ - //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{}), }, args: args{ @@ -263,9 +263,9 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { }, }, context: pkg.Context{ - Distro: &linux.Release{ - ID: "debian", - VersionID: "8", + Distro: &distro.Distro{ + Type: "debian", + Version: "8", }, }, }, @@ -273,7 +273,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "matches by exact-direct match (OS)", fields: fields{ - //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{}), }, args: args{ @@ -281,9 +280,9 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { neutron2013Pkg, }, context: pkg.Context{ - Distro: &linux.Release{ - ID: "debian", - VersionID: "8", + Distro: &distro.Distro{ + Type: "debian", + Version: "8", }, }, }, @@ -325,7 +324,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "fail on severity threshold", fields: fields{ - //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{}), FailSeverity: func() *vulnerability.Severity { x := vulnerability.LowSeverity @@ -337,9 +335,9 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { neutron2013Pkg, }, context: pkg.Context{ - Distro: &linux.Release{ - ID: "debian", - VersionID: "8", + Distro: &distro.Distro{ + Type: "debian", + Version: "8", }, }, }, @@ -381,7 +379,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "pass on severity threshold with VEX", fields: fields{ - //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{}), FailSeverity: func() *vulnerability.Severity { x := vulnerability.LowSeverity @@ -412,9 +409,9 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { }, }, }, - Distro: &linux.Release{ - ID: "debian", - VersionID: "8", + Distro: &distro.Distro{ + Type: "debian", + Version: "8", }, }, }, @@ -464,7 +461,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "matches by exact-direct match (language)", fields: fields{ - //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{ Ruby: ruby.MatcherConfig{ UseCPEs: true, @@ -561,7 +557,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "normalize by cve", fields: fields{ - //Store: str, Matchers: matcher.NewDefaultMatchers( matcher.Config{ Ruby: ruby.MatcherConfig{ @@ -645,7 +640,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "normalize by cve -- ignore GHSA", fields: fields{ - //Store: str, Matchers: matcher.NewDefaultMatchers( matcher.Config{ Ruby: ruby.MatcherConfig{ @@ -758,7 +752,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "normalize by cve -- ignore CVE", fields: fields{ - //Store: str, Matchers: matcher.NewDefaultMatchers( matcher.Config{ Ruby: ruby.MatcherConfig{ @@ -878,7 +871,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { { name: "ignore CVE (not normalized by CVE)", fields: fields{ - //Store: str, Matchers: matcher.NewDefaultMatchers(matcher.Config{ Ruby: ruby.MatcherConfig{ UseCPEs: true, @@ -985,6 +977,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { wantErr: nil, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &VulnerabilityMatcher{ @@ -1032,6 +1025,61 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { } } +func Test_fatalErrors(t *testing.T) { + tests := []struct { + name string + matcherFunc matcherMock.MatchFunc + assertErr assert.ErrorAssertionFunc + }{ + { + name: "no error", + matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return nil, nil, nil + }, + assertErr: assert.NoError, + }, + { + name: "non-fatal error", + matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return nil, nil, errors.New("some error") + }, + assertErr: assert.NoError, + }, + { + name: "fatal error", + matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + return nil, nil, match.NewFatalError(match.UnknownMatcherType, errors.New("some error")) + }, + assertErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &VulnerabilityMatcher{ + Matchers: []match.Matcher{matcherMock.New(syftPkg.JavaPkg, tt.matcherFunc)}, + } + + _, _, err := m.FindMatches([]pkg.Package{ + { + Name: "foo", + Version: "1.2.3", + Type: syftPkg.JavaPkg, + }, + }, + pkg.Context{ + Distro: &distro.Distro{ + Type: "debian", + Version: "8", + }, + }, + ) + + tt.assertErr(t, err) + }) + } +} + func Test_indexFalsePositivesByLocation(t *testing.T) { cases := []struct { name string @@ -1336,6 +1384,34 @@ func Test_filterMatchesUsingDistroFalsePositives(t *testing.T) { } } +type panicyMatcher struct { + matcherType match.MatcherType +} + +func (m *panicyMatcher) PackageTypes() []syftPkg.Type { + return nil +} + +func (m *panicyMatcher) Type() match.MatcherType { + return m.matcherType +} + +func (m *panicyMatcher) Match(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoredMatch, error) { + panic("test panic message") +} + +func TestCallMatcherSafely_RecoverFromPanic(t *testing.T) { + matcher := &panicyMatcher{ + matcherType: "test-matcher", + } + _, _, err := callMatcherSafely(matcher, nil, pkg.Package{}) + + require.Error(t, err) + assert.True(t, match.IsFatalError(err)) + require.Contains(t, err.Error(), "test panic message", "missing message") + require.Contains(t, err.Error(), "test-matcher", "missing matcher name") +} + type busListener struct { matching monitor.Matching } diff --git a/internal/cvss/metrics.go b/internal/cvss/metrics.go new file mode 100644 index 00000000000..0a8fe77ba48 --- /dev/null +++ b/internal/cvss/metrics.go @@ -0,0 +1,93 @@ +package cvss + +import ( + "fmt" + "math" + "strings" + + gocvss20 "github.com/pandatix/go-cvss/20" + gocvss30 "github.com/pandatix/go-cvss/30" + gocvss31 "github.com/pandatix/go-cvss/31" + gocvss40 "github.com/pandatix/go-cvss/40" + + "github.com/anchore/grype/grype/vulnerability" +) + +func ParseMetricsFromVector(vector string) (*vulnerability.CvssMetrics, error) { + switch { + case strings.HasPrefix(vector, "CVSS:3.0"): + cvss, err := gocvss30.ParseVector(vector) + if err != nil { + return nil, fmt.Errorf("unable to parse CVSS v3 vector: %w", err) + } + ex := roundScore(cvss.Exploitability()) + im := roundScore(cvss.Impact()) + return &vulnerability.CvssMetrics{ + BaseScore: roundScore(cvss.BaseScore()), + ExploitabilityScore: &ex, + ImpactScore: &im, + }, nil + case strings.HasPrefix(vector, "CVSS:3.1"): + cvss, err := gocvss31.ParseVector(vector) + if err != nil { + return nil, fmt.Errorf("unable to parse CVSS v3.1 vector: %w", err) + } + ex := roundScore(cvss.Exploitability()) + im := roundScore(cvss.Impact()) + return &vulnerability.CvssMetrics{ + BaseScore: roundScore(cvss.BaseScore()), + ExploitabilityScore: &ex, + ImpactScore: &im, + }, nil + case strings.HasPrefix(vector, "CVSS:4.0"): + cvss, err := gocvss40.ParseVector(vector) + if err != nil { + return nil, fmt.Errorf("unable to parse CVSS v4.0 vector: %w", err) + } + // there are no exploitability and impact scores in CVSS v4.0 + return &vulnerability.CvssMetrics{ + BaseScore: roundScore(cvss.Score()), + }, nil + default: + // should be CVSS v2.0 or is invalid + cvss, err := gocvss20.ParseVector(vector) + if err != nil { + return nil, fmt.Errorf("unable to parse CVSS v2 vector: %w", err) + } + ex := roundScore(cvss.Exploitability()) + im := roundScore(cvss.Impact()) + return &vulnerability.CvssMetrics{ + BaseScore: roundScore(cvss.BaseScore()), + ExploitabilityScore: &ex, + ImpactScore: &im, + }, nil + } +} + +func SeverityFromBaseScore(bs float64) vulnerability.Severity { + switch { + case bs >= 10.0: + return vulnerability.UnknownSeverity + case bs >= 9.0: + return vulnerability.CriticalSeverity + case bs >= 7.0: + return vulnerability.HighSeverity + case bs >= 4.0: + return vulnerability.MediumSeverity + case bs >= 0.1: + return vulnerability.LowSeverity + case bs > 0: + return vulnerability.NegligibleSeverity + } + return vulnerability.UnknownSeverity +} + +// roundScore rounds the score to the nearest tenth based on first.org rounding rules +// see https://www.first.org/cvss/v3.1/specification-document#Appendix-A---Floating-Point-Rounding +func roundScore(score float64) float64 { + intInput := int(math.Round(score * 100000)) + if intInput%10000 == 0 { + return float64(intInput) / 100000.0 + } + return (math.Floor(float64(intInput)/10000.0) + 1) / 10.0 +} diff --git a/internal/cvss/metrics_test.go b/internal/cvss/metrics_test.go new file mode 100644 index 00000000000..83e4a19c1e1 --- /dev/null +++ b/internal/cvss/metrics_test.go @@ -0,0 +1,195 @@ +package cvss + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/vulnerability" +) + +func TestParseMetricsFromVector(t *testing.T) { + tests := []struct { + name string + vector string + expectedMetrics *vulnerability.CvssMetrics + wantErr require.ErrorAssertionFunc + }{ + { + name: "valid CVSS 2.0", + vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + expectedMetrics: &vulnerability.CvssMetrics{ + BaseScore: 7.5, + ExploitabilityScore: ptr(10.0), + ImpactScore: ptr(6.5), + }, + }, + { + name: "valid CVSS 3.0", + vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + expectedMetrics: &vulnerability.CvssMetrics{ + BaseScore: 9.8, + ExploitabilityScore: ptr(3.9), + ImpactScore: ptr(5.9), + }, + }, + { + name: "valid CVSS 3.1", + vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + expectedMetrics: &vulnerability.CvssMetrics{ + BaseScore: 9.8, + ExploitabilityScore: ptr(3.9), + ImpactScore: ptr(5.9), + }, + }, + { + name: "valid CVSS 4.0", + vector: "CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:N/VI:H/VA:L/SC:L/SI:H/SA:L/MAC:L/MAT:P/MPR:N/S:N/R:A/RE:L/U:Clear", + expectedMetrics: &vulnerability.CvssMetrics{ + BaseScore: 9.1, + }, + }, + { + name: "invalid CVSS 2.0", + vector: "AV:N/AC:INVALID", + wantErr: require.Error, + }, + { + name: "invalid CVSS 3.0", + vector: "CVSS:3.0/AV:INVALID", + wantErr: require.Error, + }, + { + name: "invalid CVSS 3.1", + vector: "CVSS:3.1/AV:INVALID", + wantErr: require.Error, + }, + { + name: "invalid CVSS 4.0", + vector: "CVSS:4.0/AV:INVALID", + wantErr: require.Error, + }, + { + name: "empty vector", + vector: "", + wantErr: require.Error, + }, + { + name: "malformed vector", + vector: "INVALID:VECTOR", + wantErr: require.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + result, err := ParseMetricsFromVector(tt.vector) + tt.wantErr(t, err) + if err != nil { + assert.Nil(t, result) + return + } + + require.NotNil(t, result) + assert.Equal(t, tt.expectedMetrics.BaseScore, result.BaseScore, "given vector: %s", tt.vector) + + if tt.expectedMetrics.ExploitabilityScore != nil { + require.NotNil(t, result.ExploitabilityScore) + assert.Equal(t, *tt.expectedMetrics.ExploitabilityScore, *result.ExploitabilityScore, "given vector: %s", tt.vector) + } + + if tt.expectedMetrics.ImpactScore != nil { + require.NotNil(t, result.ImpactScore) + assert.Equal(t, *tt.expectedMetrics.ImpactScore, *result.ImpactScore, "given vector: %s", tt.vector) + } + }) + } +} + +func TestSeverityFromBaseScore(t *testing.T) { + tests := []struct { + name string + score float64 + expected vulnerability.Severity + }{ + { + name: "unknown severity (exactly 10.0)", + score: 10.0, + expected: vulnerability.UnknownSeverity, + }, + { + name: "unknown severity (greater than 10.0)", + score: 10.1, + expected: vulnerability.UnknownSeverity, + }, + { + name: "critical severity (lower bound)", + score: 9.0, + expected: vulnerability.CriticalSeverity, + }, + { + name: "critical severity (upper bound)", + score: 9.9, + expected: vulnerability.CriticalSeverity, + }, + { + name: "high severity (lower bound)", + score: 7.0, + expected: vulnerability.HighSeverity, + }, + { + name: "high severity (upper bound)", + score: 8.9, + expected: vulnerability.HighSeverity, + }, + { + name: "medium severity (lower bound)", + score: 4.0, + expected: vulnerability.MediumSeverity, + }, + { + name: "medium severity (upper bound)", + score: 6.9, + expected: vulnerability.MediumSeverity, + }, + { + name: "low severity (lower bound)", + score: 0.1, + expected: vulnerability.LowSeverity, + }, + { + name: "low severity (upper bound)", + score: 3.9, + expected: vulnerability.LowSeverity, + }, + { + name: "negligible severity (between 0 and 0.1)", + score: 0.05, + expected: vulnerability.NegligibleSeverity, + }, + { + name: "unknown severity (exactly zero)", + score: 0.0, + expected: vulnerability.UnknownSeverity, + }, + { + name: "unknown severity (negative)", + score: -1.0, + expected: vulnerability.UnknownSeverity, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, SeverityFromBaseScore(tt.score)) + }) + } +} + +func ptr(f float64) *float64 { + return &f +} diff --git a/internal/file/copy.go b/internal/file/copy.go index 4508044420d..53fcb6547c8 100644 --- a/internal/file/copy.go +++ b/internal/file/copy.go @@ -11,7 +11,7 @@ import ( func CopyDir(fs afero.Fs, src string, dst string) error { var err error - var fds []os.DirEntry + var fds []os.FileInfo // <-- afero.ReadDir returns []os.FileInfo var srcinfo os.FileInfo if srcinfo, err = fs.Stat(src); err != nil { @@ -22,7 +22,7 @@ func CopyDir(fs afero.Fs, src string, dst string) error { return err } - if fds, err = os.ReadDir(src); err != nil { + if fds, err = afero.ReadDir(fs, src); err != nil { return err } for _, fd := range fds { diff --git a/internal/format/writer.go b/internal/format/writer.go index a55351a77e4..03e65652ff4 100644 --- a/internal/format/writer.go +++ b/internal/format/writer.go @@ -9,8 +9,8 @@ import ( "strings" "github.com/hashicorp/go-multierror" - "github.com/mitchellh/go-homedir" + "github.com/anchore/go-homedir" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" diff --git a/schema/grype/db-search/json/schema-1.0.2.json b/schema/grype/db-search/json/schema-1.0.2.json new file mode 100644 index 00000000000..365c6583310 --- /dev/null +++ b/schema/grype/db-search/json/schema-1.0.2.json @@ -0,0 +1,529 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/grype/db-search/json/1.0.2/matches", + "$ref": "#/$defs/Matches", + "$defs": { + "AffectedPackageBlob": { + "$defs": { + "cves": { + "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." + }, + "qualifiers": { + "description": "are package attributes that confirm the package is affected by the vulnerability." + }, + "ranges": { + "description": "specifies the affected version ranges and fixes if available." + } + }, + "properties": { + "cves": { + "items": { + "type": "string" + }, + "type": "array" + }, + "qualifiers": { + "$ref": "#/$defs/AffectedPackageQualifiers" + }, + "ranges": { + "items": { + "$ref": "#/$defs/AffectedRange" + }, + "type": "array" + } + }, + "type": "object" + }, + "AffectedPackageInfo": { + "$defs": { + "cpe": { + "description": "is a Common Platform Enumeration that is affected by the vulnerability" + }, + "detail": { + "description": "is the detailed information about the affected package" + }, + "namespace": { + "description": "is a holdover value from the v5 DB schema that combines provider and search methods into a single value\nDeprecated: this field will be removed in a later version of the search schema" + }, + "os": { + "description": "identifies the operating system release that the affected package is released for" + }, + "package": { + "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" + } + }, + "properties": { + "os": { + "$ref": "#/$defs/OperatingSystem" + }, + "package": { + "$ref": "#/$defs/Package" + }, + "cpe": { + "$ref": "#/$defs/CPE" + }, + "namespace": { + "type": "string" + }, + "detail": { + "$ref": "#/$defs/AffectedPackageBlob" + } + }, + "type": "object", + "required": [ + "namespace", + "detail" + ] + }, + "AffectedPackageQualifiers": { + "$defs": { + "platform_cpes": { + "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." + }, + "rpm_modularity": { + "description": "indicates if the package follows RPM modularity for versioning." + } + }, + "properties": { + "rpm_modularity": { + "type": "string" + }, + "platform_cpes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "AffectedRange": { + "$defs": { + "fix": { + "description": "provides details on the fix version and its state if available." + }, + "version": { + "description": "defines the version constraints for affected software." + } + }, + "properties": { + "version": { + "$ref": "#/$defs/AffectedVersion" + }, + "fix": { + "$ref": "#/$defs/Fix" + } + }, + "type": "object" + }, + "AffectedVersion": { + "$defs": { + "constraint": { + "description": "defines the version range constraint for affected versions." + }, + "type": { + "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." + } + }, + "properties": { + "type": { + "type": "string" + }, + "constraint": { + "type": "string" + } + }, + "type": "object" + }, + "CPE": { + "properties": { + "ID": { + "type": "integer" + }, + "Part": { + "type": "string" + }, + "Vendor": { + "type": "string" + }, + "Product": { + "type": "string" + }, + "Edition": { + "type": "string" + }, + "Language": { + "type": "string" + }, + "SoftwareEdition": { + "type": "string" + }, + "TargetHardware": { + "type": "string" + }, + "TargetSoftware": { + "type": "string" + }, + "Other": { + "type": "string" + }, + "Packages": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "ID", + "Part", + "Vendor", + "Product", + "Edition", + "Language", + "SoftwareEdition", + "TargetHardware", + "TargetSoftware", + "Other", + "Packages" + ] + }, + "EPSS": { + "properties": { + "cve": { + "type": "string" + }, + "epss": { + "type": "number" + }, + "percentile": { + "type": "number" + }, + "date": { + "type": "string" + } + }, + "type": "object", + "required": [ + "cve", + "epss", + "percentile", + "date" + ] + }, + "Fix": { + "$defs": { + "detail": { + "description": "provides additional fix information, such as commit details." + }, + "state": { + "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." + }, + "version": { + "description": "is the version number of the fix." + } + }, + "properties": { + "version": { + "type": "string" + }, + "state": { + "type": "string" + }, + "detail": { + "$ref": "#/$defs/FixDetail" + } + }, + "type": "object" + }, + "FixDetail": { + "$defs": { + "git_commit": { + "description": "is the identifier for the Git commit associated with the fix." + }, + "references": { + "description": "contains URLs or identifiers for additional resources on the fix." + }, + "timestamp": { + "description": "is the date and time when the fix was committed." + } + }, + "properties": { + "git_commit": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "references": { + "items": { + "$ref": "#/$defs/Reference" + }, + "type": "array" + } + }, + "type": "object" + }, + "KnownExploited": { + "properties": { + "cve": { + "type": "string" + }, + "vendor_project": { + "type": "string" + }, + "product": { + "type": "string" + }, + "date_added": { + "type": "string" + }, + "required_action": { + "type": "string" + }, + "due_date": { + "type": "string" + }, + "known_ransomware_campaign_use": { + "type": "string" + }, + "notes": { + "type": "string" + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "cwes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "cve", + "known_ransomware_campaign_use" + ] + }, + "Match": { + "$defs": { + "packages": { + "description": "is the list of packages affected by the vulnerability." + }, + "vulnerability": { + "description": "is the core advisory record for a single known vulnerability from a specific provider." + } + }, + "properties": { + "vulnerability": { + "$ref": "#/$defs/VulnerabilityInfo" + }, + "packages": { + "items": { + "$ref": "#/$defs/AffectedPackageInfo" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "vulnerability", + "packages" + ] + }, + "Matches": { + "items": { + "$ref": "#/$defs/Match" + }, + "type": "array" + }, + "OperatingSystem": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Package": { + "properties": { + "name": { + "type": "string" + }, + "ecosystem": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "ecosystem" + ] + }, + "Reference": { + "$defs": { + "tags": { + "description": "is a free-form organizational field to convey additional information about the reference" + }, + "url": { + "description": "is the external resource" + } + }, + "properties": { + "url": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "url" + ] + }, + "Severity": { + "$defs": { + "rank": { + "description": "is a free-form organizational field to convey priority over other severities" + }, + "scheme": { + "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" + }, + "source": { + "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" + }, + "value": { + "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" + } + }, + "properties": { + "scheme": { + "type": "string" + }, + "value": true, + "source": { + "type": "string" + }, + "rank": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "scheme", + "value", + "rank" + ] + }, + "VulnerabilityInfo": { + "$defs": { + "epss": { + "description": "is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability" + }, + "known_exploited": { + "description": "is a list of known exploited vulnerabilities from the CISA KEV dataset" + }, + "modified_date": { + "description": "is the date the vulnerability record was last modified" + }, + "provider": { + "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." + }, + "published_date": { + "description": "is the date the vulnerability record was first published" + }, + "status": { + "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" + }, + "withdrawn_date": { + "description": "is the date the vulnerability record was withdrawn" + } + }, + "properties": { + "id": { + "type": "string" + }, + "assigner": { + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "refs": { + "items": { + "$ref": "#/$defs/Reference" + }, + "type": "array" + }, + "aliases": { + "items": { + "type": "string" + }, + "type": "array" + }, + "severities": { + "items": { + "$ref": "#/$defs/Severity" + }, + "type": "array" + }, + "provider": { + "type": "string" + }, + "status": { + "type": "string" + }, + "published_date": { + "type": "string", + "format": "date-time" + }, + "modified_date": { + "type": "string", + "format": "date-time" + }, + "withdrawn_date": { + "type": "string", + "format": "date-time" + }, + "known_exploited": { + "items": { + "$ref": "#/$defs/KnownExploited" + }, + "type": "array" + }, + "epss": { + "items": { + "$ref": "#/$defs/EPSS" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "id", + "provider", + "status" + ] + } + } +} diff --git a/schema/grype/db-search/json/schema-latest.json b/schema/grype/db-search/json/schema-latest.json index d1ddb779dd3..365c6583310 100644 --- a/schema/grype/db-search/json/schema-latest.json +++ b/schema/grype/db-search/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/grype/db-search/json/1.0.1/matches", + "$id": "anchore.io/schema/grype/db-search/json/1.0.2/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageBlob": { @@ -42,6 +42,9 @@ "detail": { "description": "is the detailed information about the affected package" }, + "namespace": { + "description": "is a holdover value from the v5 DB schema that combines provider and search methods into a single value\nDeprecated: this field will be removed in a later version of the search schema" + }, "os": { "description": "identifies the operating system release that the affected package is released for" }, @@ -59,12 +62,16 @@ "cpe": { "$ref": "#/$defs/CPE" }, + "namespace": { + "type": "string" + }, "detail": { "$ref": "#/$defs/AffectedPackageBlob" } }, "type": "object", "required": [ + "namespace", "detail" ] }, diff --git a/templates/html.tmpl b/templates/html.tmpl index 5fbab17f1f6..a550829375d 100644 --- a/templates/html.tmpl +++ b/templates/html.tmpl @@ -1,595 +1,1545 @@ - - - - - - Vulnerability Report - - - - - - - - - - - - - - - - - - - - - - - - - - - -{{/* Initialize counters */}} -{{- $CountCritical := 0 }} -{{- $CountHigh := 0 }} -{{- $CountMedium := 0 }} -{{- $CountLow := 0}} -{{- $CountUnknown := 0 }} - -{{/* Create a list */}} -{{- $FilteredMatches := list }} - -{{/* Loop through all vulns limit output and set count*/}} -{{- range $vuln := .Matches }} - {{/* Use this filter to exclude severity if needed */}} - {{- if or (eq $vuln.Vulnerability.Severity "Critical") (eq $vuln.Vulnerability.Severity "High") (eq $vuln.Vulnerability.Severity "Medium") (eq $vuln.Vulnerability.Severity "Low") (eq $vuln.Vulnerability.Severity "Unknown") }} - {{- $FilteredMatches = append $FilteredMatches $vuln }} - {{- if eq $vuln.Vulnerability.Severity "Critical" }} - {{- $CountCritical = add $CountCritical 1 }} - {{- else if eq $vuln.Vulnerability.Severity "High" }} - {{- $CountHigh = add $CountHigh 1 }} - {{- else if eq $vuln.Vulnerability.Severity "Medium" }} - {{- $CountMedium = add $CountMedium 1 }} - {{- else if eq $vuln.Vulnerability.Severity "Low" }} - {{- $CountLow = add $CountLow 1 }} - {{- else }} - {{- $CountUnknown = add $CountUnknown 1 }} - {{- end }} - {{- end }} -{{- end }} - - -
-
-
-

Container Vulnerability Report

-

Name: {{- if eq (.Source.Type) "image" -}} {{.Source.Target.UserInput}} - {{- else if eq (.Source.Type) "directory" -}} {{.Source.Target}} - {{- else if eq (.Source.Type) "file" -}} {{.Source.Target}} - {{- else -}} unknown - {{- end -}}

-

Type: {{ .Source.Type }}

-

Date: {{.Descriptor.Timestamp}}

-
-
- Grype Logo -
-
-
-
-
Critical
-
{{ $CountCritical }}
-
-
-
High
-
{{ $CountHigh }}
-
-
-
Medium
-
{{ $CountMedium }}
-
-
-
Low
-
{{ $CountLow }}
-
-
-
Unknown
-
{{ $CountUnknown }}
-
-
-
- - - - - - - - - - - - - - - {{- range $FilteredMatches }} - - - - - - - - - - - {{end}} - -
NameVersionTypeVulnerabilitySeverityDescriptionStateFixed In
{{.Artifact.Name}}{{.Artifact.Version}}{{.Artifact.Type}} - {{.Vulnerability.ID}} - {{.Vulnerability.Severity}}{{html .Vulnerability.Description}}{{.Vulnerability.Fix.State}} - {{- if .Vulnerability.Fix.Versions }} -
    - {{- range .Vulnerability.Fix.Versions }} -
  • {{ . }}
  • - {{- end }} -
- {{- else }} - N/A - {{- end }} -
-
-
- - - - - - - + + + + + + Vulnerability Report + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{/* Initialize counters */}} +{{- $CountCritical := 0 }} +{{- $CountHigh := 0 }} +{{- $CountMedium := 0 }} +{{- $CountLow := 0}} +{{- $CountUnknown := 0 }} + +{{/* Create a list */}} +{{- $FilteredMatches := list }} + +{{/* Loop through all vulns limit output and set count*/}} +{{- range $vuln := .Matches }} + {{/* Use this filter to exclude severity if needed */}} + {{- if or (eq $vuln.Vulnerability.Severity "Critical") (eq $vuln.Vulnerability.Severity "High") (eq $vuln.Vulnerability.Severity "Medium") (eq $vuln.Vulnerability.Severity "Low") (eq $vuln.Vulnerability.Severity "Unknown") }} + {{- $FilteredMatches = append $FilteredMatches $vuln }} + {{- if eq $vuln.Vulnerability.Severity "Critical" }} + {{- $CountCritical = add $CountCritical 1 }} + {{- else if eq $vuln.Vulnerability.Severity "High" }} + {{- $CountHigh = add $CountHigh 1 }} + {{- else if eq $vuln.Vulnerability.Severity "Medium" }} + {{- $CountMedium = add $CountMedium 1 }} + {{- else if eq $vuln.Vulnerability.Severity "Low" }} + {{- $CountLow = add $CountLow 1 }} + {{- else }} + {{- $CountUnknown = add $CountUnknown 1 }} + {{- end }} + {{- end }} +{{- end }} + + +
+
+
+

Vulnerability Report

+
+
Name:
+
{{- if eq (.Source.Type) "image" -}} {{.Source.Target.UserInput}} + {{- else if eq (.Source.Type) "directory" -}} {{.Source.Target}} + {{- else if eq (.Source.Type) "file" -}} {{.Source.Target}} + {{- else -}} unknown + {{- end -}}
+ +
Type:
+
{{ .Source.Type }}
+ + {{- /* Conditionally add ImageID (Checksum) for images */ -}} + {{- if eq .Source.Type "image" -}} + {{- with .Source.Target.ID -}} +
Checksum:
+
{{ . }}
+ {{- end -}} + {{- end -}} + +
Date:
+
+ {{.Descriptor.Timestamp}} + +
+
+
+
+ Grype Logo +
+
+
+
+
Critical
+
{{ $CountCritical }}
+
+
+
High
+
{{ $CountHigh }}
+
+
+
Medium
+
{{ $CountMedium }}
+
+
+
Low
+
{{ $CountLow }}
+
+
+
Unknown
+
{{ $CountUnknown }}
+
+
+
+ + + + + + + + + + + + + + + + + {{- range $FilteredMatches }} + + + + + + + + + + + + + {{- end }} + +
NameVersionTypeVulnerabilitySeverityStateFixed InDescriptionRelated URLsPURL
{{.Artifact.Name}}{{.Artifact.Version}}{{.Artifact.Type}} + {{.Vulnerability.ID}} + {{.Vulnerability.Severity}}{{.Vulnerability.Fix.State}} + {{- if .Vulnerability.Fix.Versions }} +
    + {{- range .Vulnerability.Fix.Versions }} +
  • {{ . }}
  • + {{- end }} +
+ {{- else }} + N/A + {{- end }} +
{{html .Vulnerability.Description}}{{ toJson .Vulnerability.URLs }}{{ .Artifact.PURL }}
+
+
+ + + + + \ No newline at end of file diff --git a/test/integration/compare_sbom_input_vs_lib_test.go b/test/integration/compare_sbom_input_vs_lib_test.go index 80e0db48ed6..eafc94cdbc2 100644 --- a/test/integration/compare_sbom_input_vs_lib_test.go +++ b/test/integration/compare_sbom_input_vs_lib_test.go @@ -54,6 +54,7 @@ func TestCompareSBOMInputToLibResults(t *testing.T) { string(syftPkg.BinaryPkg), // these are removed due to overlap-by-file-ownership string(syftPkg.BitnamiPkg), string(syftPkg.PhpPeclPkg), + string(syftPkg.PhpPearPkg), string(syftPkg.RustPkg), string(syftPkg.KbPkg), string(syftPkg.DartPubPkg), @@ -62,6 +63,7 @@ func TestCompareSBOMInputToLibResults(t *testing.T) { string(syftPkg.ConanPkg), string(syftPkg.HexPkg), string(syftPkg.PortagePkg), + string(syftPkg.HomebrewPkg), string(syftPkg.CocoapodsPkg), string(syftPkg.HackagePkg), string(syftPkg.NixPkg), diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 6b43eb9cf45..9a226cc2157 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/grype/grype" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/matcher/dotnet" @@ -30,7 +31,6 @@ import ( "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/cataloging/pkgcataloging" "github.com/anchore/syft/syft/cpe" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -177,7 +177,21 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/dotnet/TestLibrary.deps.json") - if len(packages) != 2 { // TestLibrary + AWSSDK.Core + // 55caef8df7ac822e Pkg(name="TestLibrary" version="1.0.0" type="dotnet" id="55caef8df7ac822e") + // 0012329cdebba0ea Pkg(name="AWSSDK.Core" version="3.7.10.6" type="dotnet" id="0012329cdebba0ea") + // 07ec6fb2adb2cf8f Pkg(name="Microsoft.Extensions.DependencyInjection.Abstractions" version="6.0.0" type="dotnet" id="07ec6fb2adb2cf8f") + // ff03e77b91acca32 Pkg(name="Microsoft.Extensions.DependencyInjection" version="6.0.0" type="dotnet" id="ff03e77b91acca32") + // a1ea42c8f064083e Pkg(name="Microsoft.Extensions.Logging.Abstractions" version="6.0.0" type="dotnet" id="a1ea42c8f064083e") + // aaef85a2649e5d15 Pkg(name="Microsoft.Extensions.Logging" version="6.0.0" type="dotnet" id="aaef85a2649e5d15") + // 4af0fb6a81ba0423 Pkg(name="Microsoft.Extensions.Options" version="6.0.0" type="dotnet" id="4af0fb6a81ba0423") + // cb41a8aefdf40c3a Pkg(name="Microsoft.Extensions.Primitives" version="6.0.0" type="dotnet" id="cb41a8aefdf40c3a") + // 5ee80fba9caa3ab3 Pkg(name="Newtonsoft.Json" version="13.0.1" type="dotnet" id="5ee80fba9caa3ab3") + // df4b5dc73acd1f36 Pkg(name="Serilog.Sinks.Console" version="4.0.1" type="dotnet" id="df4b5dc73acd1f36") + // 023b9ba74c5c5ef5 Pkg(name="Serilog" version="2.10.0" type="dotnet" id="023b9ba74c5c5ef5") + // 430e4d4304a3ff55 Pkg(name="System.Diagnostics.DiagnosticSource" version="6.0.0" type="dotnet" id="430e4d4304a3ff55") + // 42021023d8f87661 Pkg(name="System.Runtime.CompilerServices.Unsafe" version="6.0.0" type="dotnet" id="42021023d8f87661") + // 2bb01d8c22df1e95 Pkg(name="TestCommon" version="1.0.0" type="dotnet" id="2bb01d8c22df1e95") + if len(packages) != 14 { for _, p := range packages { t.Logf("Dotnet Package: %s %+v", p.ID(), p) } @@ -773,7 +787,7 @@ func TestMatchByImage(t *testing.T) { }, }) - actualResults := grype.FindVulnerabilitiesForPackage(theProvider, s.Artifacts.LinuxDistribution, matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})) + actualResults := grype.FindVulnerabilitiesForPackage(theProvider, distro.FromRelease(s.Artifacts.LinuxDistribution), matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})) for _, m := range actualResults.Sorted() { for _, d := range m.Details { observedMatchers.Add(string(d.Matcher)) @@ -931,7 +945,6 @@ func vexMatches(t *testing.T, ignoredMatches []match.IgnoredMatch, vexStatus vex }, }, }, - Distro: &linux.Release{}, } vexedMatches, ignoredMatches, err := vexMatcher.ApplyVEX(pctx, &matches, ignoredMatches) diff --git a/test/quality/test-db b/test/quality/test-db index 3073ef8e0e6..75c7e136081 100644 --- a/test/quality/test-db +++ b/test/quality/test-db @@ -1 +1 @@ -vulnerability-db_v6.0.2_2025-02-28T01:30:50Z_1740715588.tar.zst +vulnerability-db_v6.0.2_2025-05-01T01:31:33Z_1746072708.tar.zst