diff --git a/.binny.yaml b/.binny.yaml
new file mode 100644
index 00000000000..f194c0b9699
--- /dev/null
+++ b/.binny.yaml
@@ -0,0 +1,111 @@
+tools:
+ # we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)
+ - name: binny
+ version:
+ want: v0.9.0
+ method: github-release
+ with:
+ repo: anchore/binny
+
+ # used to produce SBOMs during release
+ - name: syft
+ version:
+ want: latest
+ method: github-release
+ with:
+ repo: anchore/syft
+
+ # used to sign mac binaries at release
+ - name: quill
+ version:
+ want: v0.5.1
+ method: github-release
+ with:
+ repo: anchore/quill
+
+ # used for linting
+ - name: golangci-lint
+ version:
+ want: v2.1.6
+ method: github-release
+ with:
+ repo: golangci/golangci-lint
+
+ # used for showing the changelog at release
+ - name: glow
+ version:
+ want: v2.1.0
+ method: github-release
+ with:
+ repo: charmbracelet/glow
+
+ # used for signing the checksums file at release
+ - name: cosign
+ version:
+ want: v2.5.0
+ method: github-release
+ with:
+ repo: sigstore/cosign
+
+ # used to release all artifacts
+ - name: goreleaser
+ version:
+ want: v2.9.0
+ method: github-release
+ with:
+ repo: goreleaser/goreleaser
+
+ # used for organizing imports during static analysis
+ - name: gosimports
+ version:
+ want: v0.3.8
+ method: github-release
+ with:
+ repo: rinchsan/gosimports
+
+ # used at release to generate the changelog
+ - name: chronicle
+ version:
+ want: v0.8.0
+ method: github-release
+ with:
+ repo: anchore/chronicle
+
+ # used during static analysis for license compliance
+ - name: bouncer
+ version:
+ want: v0.4.0
+ method: github-release
+ with:
+ repo: wagoodman/go-bouncer
+
+ # used for running all local and CI tasks
+ - name: task
+ version:
+ want: v3.43.3
+ method: github-release
+ with:
+ repo: go-task/task
+
+ # used for triggering a release
+ - name: gh
+ version:
+ want: v2.73.0
+ method: github-release
+ with:
+ repo: cli/cli
+
+ # used for integration tests
+ - name: skopeo
+ version:
+ want: v1.19.0
+ method: go-install
+ with:
+ module: github.com/containers/skopeo
+ entrypoint: cmd/skopeo
+ args:
+ - "-tags"
+ - containers_image_openpgp
+ env:
+ - CGO_ENABLED=0
+ - GO_DYN_FLAGS=""
diff --git a/.chronicle.yaml b/.chronicle.yaml
index 400437c89ad..a2e6c04bd65 100644
--- a/.chronicle.yaml
+++ b/.chronicle.yaml
@@ -1 +1,2 @@
enforce-v0: true # don't make breaking-change label bump major version before 1.0.
+title: ""
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index d02615a57bc..b0663218fb4 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -13,6 +13,18 @@ assignees: ''
**How to reproduce it (as minimally and precisely as possible)**:
+
**Anything else we need to know?**:
**Environment**:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index edd71d50485..3009b9c0cc0 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,6 +1,6 @@
contact_links:
- - name: Join the Slack community 💬
- # link to our community Slack registration page
- url: https://anchore.com/slack
+ - name: Join our Discourse community 💬
+ # link to our community Discourse site
+ url: https://anchore.com/discourse
about: 'Come chat with us! Ask for help, join our software development efforts, or just give us feedback!'
diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml
index 70adc84d816..3a6a906469e 100644
--- a/.github/actions/bootstrap/action.yaml
+++ b/.github/actions/bootstrap/action.yaml
@@ -4,23 +4,26 @@ inputs:
go-version:
description: "Go version to install"
required: true
- default: "1.19.x"
+ default: ">= 1.24"
python-version:
description: "Python version to install"
required: true
- default: "3.10"
- use-go-cache:
- description: "Restore go cache"
+ default: "3.11"
+ go-dependencies:
+ description: "Download go dependencies"
required: true
default: "true"
cache-key-prefix:
description: "Prefix all cache keys with this value"
required: true
- default: "831180ac25"
- build-cache-key-prefix:
- description: "Prefix build cache key with this value"
+ default: "1ac8281053"
+ compute-fingerprints:
+ description: "Compute test fixture fingerprints"
required: true
- default: "f8b6d31dea"
+ default: "true"
+ tools:
+ description: "whether to install tools"
+ default: "true"
bootstrap-apt-packages:
description: "Space delimited list of tools to install via apt"
default: "libxml2-utils"
@@ -28,65 +31,33 @@ inputs:
runs:
using: "composite"
steps:
- - uses: actions/setup-go@v3
+ # note: go mod and build is automatically cached on default with v4+
+ - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
+ if: inputs.go-version != ''
with:
go-version: ${{ inputs.go-version }}
- - uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ inputs.python-version }}
- - name: Restore python cache
- id: python-venv-cache
- uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v3.2.6
- with:
- path: |
- test/quality/venv
- test/quality/vulnerability-match-labels/venv
- key: ${{ runner.os }}-python-${{ inputs.python-version }}-${{ hashFiles('**/test/quality/**/requirements.txt') }}
- restore-keys: |
- ${{ runner.os }}-python-${{ env.python-version }}-
-
- name: Restore tool cache
id: tool-cache
- uses: actions/cache@v3
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ if: inputs.tools == 'true'
with:
- path: ${{ github.workspace }}/.tmp
- key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
+ path: ${{ github.workspace }}/.tool
+ key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('.binny.yaml') }}
- # note: we need to keep restoring the go mod cache before bootstrapping tools since `go install` is used in
- # some installations of project tools.
- - name: Restore go module cache
- id: go-mod-cache
- if: inputs.use-go-cache == 'true'
- uses: actions/cache@v3
- with:
- path: |
- ~/go/pkg/mod
- key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }}
- restore-keys: |
- ${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-
-
- - name: (cache-miss) Bootstrap project tools
+ - name: Install project tools
+ if: inputs.tools == 'true'
shell: bash
- if: steps.tool-cache.outputs.cache-hit != 'true'
- run: make bootstrap-tools
-
- - name: Restore go build cache
- id: go-cache
- if: inputs.use-go-cache == 'true'
- uses: actions/cache@v3
- with:
- path: |
- ~/.cache/go-build
- key: ${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }}
- restore-keys: |
- ${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-
+ run: make tools
- - name: (cache-miss) Bootstrap go dependencies
+ - name: Install go dependencies
+ if: inputs.go-dependencies == 'true'
shell: bash
- if: steps.go-mod-cache.outputs.cache-hit != 'true' && inputs.use-go-cache == 'true'
- run: make bootstrap-go
+ run: make ci-bootstrap-go
- name: Install apt packages
if: inputs.bootstrap-apt-packages != ''
@@ -95,5 +66,6 @@ runs:
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y ${{ inputs.bootstrap-apt-packages }}
- name: Create all cache fingerprints
+ if: inputs.compute-fingerprints == 'true'
shell: bash
run: make fingerprints
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
index df401370796..613044964f5 100644
--- a/.github/dependabot.yaml
+++ b/.github/dependabot.yaml
@@ -4,7 +4,16 @@ updates:
directory: "/"
schedule:
interval: daily
+
+ - package-ecosystem: "github-actions"
+ directory: "/.github/actions/bootstrap"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 10
+ labels:
+ - "dependencies"
+
- package-ecosystem: "gomod"
directory: "/"
schedule:
- interval: daily
\ No newline at end of file
+ interval: daily
diff --git a/.github/scripts/check-syft-version-is-release.sh b/.github/scripts/check-syft-version-is-release.sh
new file mode 100755
index 00000000000..d01a30f1a19
--- /dev/null
+++ b/.github/scripts/check-syft-version-is-release.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -e
+
+version=$(grep -E "github.com/anchore/syft" go.mod | awk '{print $NF}')
+
+# ensure that the version is a release version (not a commit hash)
+# a release in this case means that the go tooling resolved the version to a tag
+# this does not guarantee that the tag has a github release associated with it
+if [[ ! $version =~ ^v[0-9]+\.[0-9]+\.[0-9]?$ ]]; then
+ echo "syft version in go.mod is not a release version: $version"
+ echo "please update the version in go.mod to a release version and try again"
+ exit 1
+else
+ echo "syft version in go.mod is a release version: $version"
+fi
diff --git a/.github/scripts/go-mod-tidy-check.sh b/.github/scripts/go-mod-tidy-check.sh
index 41bc63910bb..28f22fcdc43 100755
--- a/.github/scripts/go-mod-tidy-check.sh
+++ b/.github/scripts/go-mod-tidy-check.sh
@@ -4,19 +4,18 @@ set -eu
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
-trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
+trap "cp -p ${ORIGINAL_STATE_DIR}/* ./ && git update-index -q --refresh && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
-echo "Capturing original state of files..."
-cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
+# capturing original state of files...
+cp go.mod go.sum "${ORIGINAL_STATE_DIR}"
-echo "Capturing state of go.mod and go.sum after running go mod tidy..."
+# capturing state of go.mod and go.sum after running go mod tidy...
go mod tidy
-cp -v go.mod go.sum "${TIDY_STATE_DIR}"
-echo ""
+cp go.mod go.sum "${TIDY_STATE_DIR}"
set +e
-# Detect difference between the git HEAD state and the go mod tidy state
+# detect difference between the git HEAD state and the go mod tidy state
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
diff --git a/.github/scripts/json-schema-drift-check.sh b/.github/scripts/json-schema-drift-check.sh
new file mode 100755
index 00000000000..3002236d68b
--- /dev/null
+++ b/.github/scripts/json-schema-drift-check.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -u
+
+if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then
+ echo " 🔴 there are uncommitted changes, please commit them before running this check"
+ exit 1
+fi
+
+if ! make generate-json-schema; then
+ echo "Generating json schema failed"
+ exit 1
+fi
+
+if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then
+ echo " 🔴 there are uncommitted changes, please commit them before running this check"
+ exit 1
+fi
diff --git a/.github/scripts/syft-released-version-check.sh b/.github/scripts/syft-released-version-check.sh
deleted file mode 100755
index 73b7cf6fc10..00000000000
--- a/.github/scripts/syft-released-version-check.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env sh
-
-SYFT_LINE=$(cat go.mod | grep github.com/anchore/syft)
-
-if [ "$(echo $SYFT_LINE | grep -o '-' | wc -l)" -gt "1" ]; then
- echo "syft version is not a released version! $SYFT_LINE"
- exit 1
-else
- echo 'syft version is a released version!'
-fi
\ No newline at end of file
diff --git a/.github/scripts/trigger-release.sh b/.github/scripts/trigger-release.sh
index c1a5432efa0..208dc004550 100755
--- a/.github/scripts/trigger-release.sh
+++ b/.github/scripts/trigger-release.sh
@@ -4,12 +4,22 @@ set -eu
bold=$(tput bold)
normal=$(tput sgr0)
-if ! [ -x "$(command -v gh)" ]; then
- echo "The GitHub CLI could not be found. To continue follow the instructions at https://github.com/cli/cli#installation"
+GH_CLI=.tool/gh
+
+if ! [ -x "$(command -v $GH_CLI)" ]; then
+ echo "The GitHub CLI could not be found. run: make bootstrap"
exit 1
fi
-gh auth status
+# we want to stop the release as early as possible if the version is not a release version
+./.github/scripts/check-syft-version-is-release.sh
+
+$GH_CLI auth status
+
+# set the default repo in cases where multiple remotes are defined
+$GH_CLI repo set-default anchore/grype
+
+export GITHUB_TOKEN="${GITHUB_TOKEN-"$($GH_CLI auth token)"}"
# we need all of the git state to determine the next version. Since tagging is done by
# the release pipeline it is possible to not have all of the tags from previous releases.
@@ -37,7 +47,7 @@ done
echo "${bold}Kicking off release for ${NEXT_VERSION}${normal}..."
echo
-gh workflow run release.yaml -f version=${NEXT_VERSION}
+$GH_CLI workflow run release.yaml -f version=${NEXT_VERSION}
echo
echo "${bold}Waiting for release to start...${normal}"
@@ -45,6 +55,6 @@ sleep 10
set +e
-echo "${bold}Head to the release workflow to monitor the release:${normal} $(gh run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')"
-id=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId')
-gh run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" gh run view $id --log-failed)
+echo "${bold}Head to the release workflow to monitor the release:${normal} $($GH_CLI run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')"
+id=$($GH_CLI run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId')
+$GH_CLI run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" $GH_CLI run view $id --log-failed)
diff --git a/.github/scripts/update-version-file.sh b/.github/scripts/update-version-file.sh
index 9c3b56d0e23..8481f25417a 100755
--- a/.github/scripts/update-version-file.sh
+++ b/.github/scripts/update-version-file.sh
@@ -2,8 +2,8 @@
set -ue
BIN="grype"
-DISTDIR=$1
-VERSION=$2
+VERSION_FILE="VERSION"
+VERSION=$1
# the source of truth as to whether we want to notify users of an update is if the release just created is NOT
# flagged as a pre-release on github
@@ -12,10 +12,9 @@ if [[ "$(curl -SsL https://api.github.com/repos/anchore/${BIN}/releases/tags/${V
exit 0
fi
-echo "creating and publishing version file"
+echo "creating and publishing version file (${VERSION})"
# create a version file for version-update checks
-VERSION_FILE="${DISTDIR}/VERSION"
echo "${VERSION}" | tee "${VERSION_FILE}"
# upload the version file that supports the application version update check
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index f59426d4859..89657d393f1 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -43,10 +43,10 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Utilize Go Module Cache
- uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v3.2.6
+ 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@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
+ uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
- go-version: '1.19'
+ go-version: '1.21'
check-latest: true
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
+ 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@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
+ uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
diff --git a/.github/workflows/dependabot-automation.yaml b/.github/workflows/dependabot-automation.yaml
new file mode 100644
index 00000000000..1c3ded297c7
--- /dev/null
+++ b/.github/workflows/dependabot-automation.yaml
@@ -0,0 +1,10 @@
+name: Dependabot Automation
+on:
+ pull_request:
+
+permissions:
+ pull-requests: write
+
+jobs:
+ run:
+ uses: anchore/workflows/.github/workflows/dependabot-automation.yaml@main
diff --git a/.github/workflows/oss-project-board-add.yaml b/.github/workflows/oss-project-board-add.yaml
index b0d1fca007a..f54cb6a0a02 100644
--- a/.github/workflows/oss-project-board-add.yaml
+++ b/.github/workflows/oss-project-board-add.yaml
@@ -1,5 +1,8 @@
name: Add to OSS board
+permissions:
+ contents: read
+
on:
issues:
types:
diff --git a/.github/workflows/release-version-file.yaml b/.github/workflows/release-version-file.yaml
new file mode 100644
index 00000000000..843b93d576b
--- /dev/null
+++ b/.github/workflows/release-version-file.yaml
@@ -0,0 +1,35 @@
+name: "Release"
+
+permissions:
+ contents: read
+
+on:
+
+ workflow_dispatch:
+ inputs:
+ version:
+ description: release version to update the version file with (prefixed with v)
+ required: true
+
+ workflow_call:
+ inputs:
+ version:
+ type: string
+ description: release version to update the version file with (prefixed with v)
+ required: true
+
+jobs:
+
+ release:
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
+
+ - name: Update version file
+ run: make ci-release-version-file
+ env:
+ RELEASE_VERSION: ${{ github.event.inputs.version }}
+ # for updating the VERSION file in S3...
+ AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
+
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 1294ce704c6..1c17ee739cf 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -6,15 +6,28 @@ on:
description: tag the latest commit on main with the given version (prefixed with v)
required: true
-env:
- GO_VERSION: "1.19.x"
+permissions:
+ contents: read
jobs:
quality-gate:
environment: release
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
+
+ - name: Check if running on main
+ if: github.ref != 'refs/heads/main'
+ # we are using the following flag when running `cosign blob-verify` for checksum signature verification:
+ # --certificate-identity-regexp "https://github.com/anchore/.github/workflows/release.yaml@refs/heads/main"
+ # if we are not on the main branch, the signature will not be verifiable since the suffix requires the main branch
+ # at the time of when the OIDC token was issued on the Github Actions runner.
+ run: echo "This can only be run on the main branch otherwise releases produced will not be verifiable with cosign" && exit 1
+
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
+
+ - name: Check if pinned syft is a release version
+ run: .github/scripts/check-syft-version-is-release.sh
- name: Check if tag already exists
# note: this will fail if the tag already exists
@@ -23,7 +36,7 @@ jobs:
git tag ${{ github.event.inputs.version }}
- name: Check static analysis results
- uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
+ uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: static-analysis
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -32,7 +45,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check unit test results
- uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
+ uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: unit
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -41,16 +54,27 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check integration test results
- uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
+ uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: integration
with:
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)
- uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
+ uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: acceptance-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -59,7 +83,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (mac)
- uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
+ uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: acceptance-mac
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -68,7 +92,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check cli test results (linux)
- uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
+ uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: cli-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -77,24 +101,30 @@ 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 }}"
false
+ # only release core assets within the "release" job. Any other assets not already under the purview of the
+ # goreleaser configuration should be added as separate jobs to allow for debugging separately from the release workflow
+ # as well as not accidentally be re-run as a step multiple times (as could be done within the release workflow) as
+ # not all actions are guaranteed to be idempotent.
release:
needs: [quality-gate]
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
permissions:
contents: write
packages: write
+ id-token: write
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v2.5.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
fetch-depth: 0
@@ -105,13 +135,13 @@ jobs:
build-cache-key-prefix: "snapshot"
- name: Login to Docker Hub
- uses: docker/login-action@v2
+ uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
with:
- username: ${{ secrets.TOOLBOX_DOCKER_USER }}
- password: ${{ secrets.TOOLBOX_DOCKER_PASS }}
+ username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}
+ password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -119,7 +149,9 @@ jobs:
- name: Tag release
run: |
- git tag ${{ github.event.inputs.version }}
+ git config user.name "anchoreci"
+ git config user.email "anchoreci@users.noreply.github.com"
+ git tag -a ${{ github.event.inputs.version }} -m "Release ${{ github.event.inputs.version }}"
git push origin --tags
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -136,18 +168,14 @@ jobs:
# for creating the release (requires write access to packages and content)
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# for updating brew formula in anchore/homebrew-syft
- GITHUB_BREW_TOKEN: ${{ secrets.ANCHORE_GIT_READ_TOKEN }}
- # for updating the VERSION file in S3...
- AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
- AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
-
+ GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- - uses: anchore/sbom-action@78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1 # v0.14.3
+ - uses: anchore/sbom-action@e11c554f704a0b820cbf8c51673f6945e0731532 # v0.20.0
continue-on-error: true
with:
artifact-name: sbom.spdx.json
- - uses: 8398a7/action-slack@fbd6aa58ba854a740e11a35d0df80cb5d12101d8 # v3.15.1
+ - uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e # v3.18.0
continue-on-error: true
with:
status: ${{ job.status }}
@@ -157,7 +185,9 @@ jobs:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ success() }}
- - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
- with:
- path: dist/**/*
- name: artifacts
+ release-version-file:
+ needs: [release]
+ uses: ./.github/workflows/release-version-file.yaml
+ with:
+ version: ${{ github.event.inputs.version }}
+ secrets: inherit
diff --git a/.github/workflows/remove-awaiting-response-label.yaml b/.github/workflows/remove-awaiting-response-label.yaml
new file mode 100644
index 00000000000..0f3b50c4f41
--- /dev/null
+++ b/.github/workflows/remove-awaiting-response-label.yaml
@@ -0,0 +1,11 @@
+name: "Manage Awaiting Response Label"
+
+on:
+ issue_comment:
+ types: [created]
+
+jobs:
+ run:
+ uses: "anchore/workflows/.github/workflows/remove-awaiting-response-label.yaml@main"
+ secrets:
+ token: ${{ secrets.OSS_PROJECT_GH_TOKEN }}
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 7bac78bdee0..8d90e6f855e 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -20,12 +20,12 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # tag=v3.0.0
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Run analysis"
- uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # tag=v2.2.0
+ uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
with:
results_file: results.sarif
results_format: sarif
@@ -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@cdcdbb579706841c47f7063dda365e292e5cad7a # tag=v1.0.26
+ uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v1.0.26
with:
sarif_file: results.sarif
diff --git a/.github/workflows/update-anchore-dependencies.yml b/.github/workflows/update-anchore-dependencies.yml
new file mode 100644
index 00000000000..bf0a3d19025
--- /dev/null
+++ b/.github/workflows/update-anchore-dependencies.yml
@@ -0,0 +1,49 @@
+name: PR to update Anchore dependencies
+on:
+ workflow_dispatch:
+ inputs:
+ repos:
+ description: "List of dependencies to update"
+ required: true
+ type: string
+
+permissions:
+ contents: read
+
+jobs:
+ update:
+ runs-on: ubuntu-latest
+ if: github.repository_owner == 'anchore' # only run for main repo (not forks)
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
+
+ - name: Bootstrap environment
+ uses: ./.github/actions/bootstrap
+ with:
+ tools: false
+ bootstrap-apt-packages: ""
+
+ - name: Update dependencies
+ id: update
+ uses: anchore/workflows/.github/actions/update-go-dependencies@main
+ with:
+ repos: ${{ github.event.inputs.repos }}
+
+ - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
+ id: generate-token
+ with:
+ app_id: ${{ secrets.TOKEN_APP_ID }}
+ private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
+
+ - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
+ with:
+ signoff: true
+ delete-branch: true
+ draft: ${{ steps.update.outputs.draft }}
+ # do not change this branch, as other workflows depend on it
+ branch: auto/integration
+ labels: dependencies,pre-release
+ commit-message: "chore(deps): update anchore dependencies"
+ title: "chore(deps): update anchore dependencies"
+ body: ${{ steps.update.outputs.summary }}
+ token: ${{ steps.generate-token.outputs.token }}
diff --git a/.github/workflows/update-bootstrap-tools.yml b/.github/workflows/update-bootstrap-tools.yml
index 8287c678ee6..5213633f459 100644
--- a/.github/workflows/update-bootstrap-tools.yml
+++ b/.github/workflows/update-bootstrap-tools.yml
@@ -1,14 +1,10 @@
-name: PR for latest versions of bootstrap tools
+name: PR for latest versions of tools
on:
schedule:
- cron: "0 8 * * *" # 3 AM EST
workflow_dispatch:
-env:
- GO_VERSION: "1.19.x"
- GO_STABLE_VERSION: true
-
permissions:
contents: read
@@ -17,61 +13,54 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'anchore/grype' # only run for main repo
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v2.5.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
+ - name: Bootstrap environment
+ uses: ./.github/actions/bootstrap
with:
- go-version: ${{ env.GO_VERSION }}
- stable: ${{ env.GO_STABLE_VERSION }}
-
- - run: |
- GOLANGCILINT_LATEST_VERSION=$(go list -m -json github.com/golangci/golangci-lint@latest 2>/dev/null | jq -r '.Version')
- BOUNCER_LATEST_VERSION=$(go list -m -json github.com/wagoodman/go-bouncer@latest 2>/dev/null | jq -r '.Version')
- CHRONICLE_LATEST_VERSION=$(go list -m -json github.com/anchore/chronicle@latest 2>/dev/null | jq -r '.Version')
- GORELEASER_LATEST_VERSION=$(go list -m -json github.com/goreleaser/goreleaser@latest 2>/dev/null | jq -r '.Version')
- GOSIMPORTS_LATEST_VERSION=$(go list -m -json github.com/rinchsan/gosimports@latest 2>/dev/null | jq -r '.Version')
- YAJSV_LATEST_VERSION=$(go list -m -json github.com/neilpa/yajsv@latest 2>/dev/null | jq -r '.Version')
- GLOW_LATEST_VERSION=$(go list -m -json github.com/charmbracelet/glow@latest 2>/dev/null | jq -r '.Version')
-
- # update version variables in the Makefile
- sed -r -i -e 's/^(GOLANGCILINT_VERSION := ).*/\1'${GOLANGCILINT_LATEST_VERSION}'/' Makefile
- sed -r -i -e 's/^(BOUNCER_VERSION := ).*/\1'${BOUNCER_LATEST_VERSION}'/' Makefile
- sed -r -i -e 's/^(CHRONICLE_VERSION := ).*/\1'${CHRONICLE_LATEST_VERSION}'/' Makefile
- sed -r -i -e 's/^(GORELEASER_VERSION := ).*/\1'${GORELEASER_LATEST_VERSION}'/' Makefile
- sed -r -i -e 's/^(GOSIMPORTS_VERSION := ).*/\1'${GOSIMPORTS_LATEST_VERSION}'/' Makefile
- sed -r -i -e 's/^(YAJSV_VERSION := ).*/\1'${YAJSV_LATEST_VERSION}'/' Makefile
- sed -r -i -e 's/^(GLOW_VERSION := ).*/\1'${GLOW_LATEST_VERSION}'/' Makefile
+ bootstrap-apt-packages: ""
+ compute-fingerprints: "false"
+ go-dependencies: false
- # export the versions for use with create-pull-request
- echo "GOLANGCILINT=$GOLANGCILINT_LATEST_VERSION" >> $GITHUB_OUTPUT
- echo "BOUNCER=$BOUNCER_LATEST_VERSION" >> $GITHUB_OUTPUT
- echo "CHRONICLE=$CHRONICLE_LATEST_VERSION" >> $GITHUB_OUTPUT
- echo "GORELEASER=$GORELEASER_LATEST_VERSION" >> $GITHUB_OUTPUT
- echo "GOSIMPORTS=$GOSIMPORTS_LATEST_VERSION" >> $GITHUB_OUTPUT
- echo "YAJSV=$YAJSV_LATEST_VERSION" >> $GITHUB_OUTPUT
- echo "GLOW=GLOW_LATEST_VERSION" >> $GITHUB_OUTPUT
+ - name: "Update tool versions"
id: latest-versions
+ run: |
+ make update-tools
+ make list-tools
+
+ export NO_COLOR=1
+ delimiter="$(openssl rand -hex 8)"
+
+ {
+ echo "status<<${delimiter}"
+ make list-tool-updates
+ echo "${delimiter}"
+ } >> $GITHUB_OUTPUT
+
+ {
+ echo "### Tool version status"
+ echo "\`\`\`"
+ make list-tool-updates
+ echo "\`\`\`"
+ } >> $GITHUB_STEP_SUMMARY
- - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
+ - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
id: generate-token
with:
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2
+ - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
with:
signoff: true
delete-branch: true
- branch: auto/latest-bootstrap-tools
+ branch: auto/latest-tools
labels: dependencies
- commit-message: 'chore(deps): update bootstrap tools to latest versions'
- title: 'chore(deps): update bootstrap tools to latest versions'
+ commit-message: 'chore(deps): update tools to latest versions'
+ title: 'chore(deps): update tools to latest versions'
body: |
- - [golangci-lint ${{ steps.latest-versions.outputs.GOLANGCILINT }}](https://github.com/golangci/golangci-lint/releases/tag/${{ steps.latest-versions.outputs.GOLANGCILINT }})
- - [bouncer ${{ steps.latest-versions.outputs.BOUNCER }}](https://github.com/wagoodman/go-bouncer/releases/tag/${{ steps.latest-versions.outputs.BOUNCER }})
- - [chronicle ${{ steps.latest-versions.outputs.CHRONICLE }}](https://github.com/anchore/chronicle/releases/tag/${{ steps.latest-versions.outputs.CHRONICLE }})
- - [goreleaser ${{ steps.latest-versions.outputs.GORELEASER }}](https://github.com/goreleaser/goreleaser/releases/tag/${{ steps.latest-versions.outputs.GORELEASER }})
- - [gosimports ${{ steps.latest-versions.outputs.GOSIMPORTS }}](https://github.com/rinchsan/gosimports/releases/tag/${{ steps.latest-versions.outputs.GOSIMPORTS }})
- - [yajsv ${{ steps.latest-versions.outputs.YAJSV }}](https://github.com/neilpa/yajsv/releases/tag/${{ steps.latest-versions.outputs.YAJSV }})
- This is an auto-generated pull request to update all of the bootstrap tools to the latest versions.
+ ```
+ ${{ steps.latest-versions.outputs.status }}
+ ```
+ This is an auto-generated pull request to update all of the tools to the latest versions.
token: ${{ steps.generate-token.outputs.token }}
diff --git a/.github/workflows/update-quality-gate-db.yml b/.github/workflows/update-quality-gate-db.yml
new file mode 100644
index 00000000000..21899b52b6d
--- /dev/null
+++ b/.github/workflows/update-quality-gate-db.yml
@@ -0,0 +1,38 @@
+name: PR for upgrading quality gate test DB
+on:
+ schedule:
+ - cron: "0 16 1 * *" # first day of each month @ 11 AM EST
+
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ update-test-db-url:
+ runs-on: ubuntu-latest
+ if: github.repository == 'anchore/grype' # only run for main repo
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
+
+ - name: "Update quality DB"
+ run: |
+ make update-quality-gate-db
+
+ - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
+ id: generate-token
+ with:
+ app_id: ${{ secrets.TOKEN_APP_ID }}
+ private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
+
+ - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
+ with:
+ signoff: true
+ delete-branch: true
+ branch: auto/update-quality-test-db
+ labels: test, changelog-ignore
+ commit-message: 'test: update quality gate db to latest version'
+ title: 'test: update quality gate db to latest version'
+ body: |
+ This is an auto-generated pull request to update the quality gate db to latest version
+ token: ${{ steps.generate-token.outputs.token }}
diff --git a/.github/workflows/update-syft-release.yml b/.github/workflows/update-syft-release.yml
deleted file mode 100644
index e051dd4144f..00000000000
--- a/.github/workflows/update-syft-release.yml
+++ /dev/null
@@ -1,57 +0,0 @@
-name: PR for latest Syft release
-on:
- schedule:
- - cron: "0 8 * * *" # 3 AM EST
-
- workflow_dispatch:
-
-env:
- GO_VERSION: "1.19.x"
- GO_STABLE_VERSION: true
-
-permissions:
- contents: read
-
-jobs:
- upgrade-syft:
- runs-on: ubuntu-latest
- if: github.repository == 'anchore/grype' # only run for main repo
- steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v2.5.0
-
- - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
- with:
- go-version: ${{ env.GO_VERSION }}
- stable: ${{ env.GO_STABLE_VERSION }}
-
- - name: Get latest Syft version
- id: latest-version
- env:
- GITHUB_TOKEN: ${{ github.token }}
- run: |
- LATEST_VERSION=$(gh release view --json name -q '.name' -R anchore/syft)
-
- # update go.mod
- go get github.com/anchore/syft@$LATEST_VERSION
- go mod tidy
-
- # export the version for use with create-pull-request
- echo "LATEST_VERSION=$LATEST_VERSION" >> $GITHUB_OUTPUT
-
- - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
- id: generate-token
- with:
- app_id: ${{ secrets.TOKEN_APP_ID }}
- private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
-
- - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2
- with:
- signoff: true
- delete-branch: true
- branch: auto/latest-syft
- labels: dependencies
- commit-message: "chore(deps): update Syft to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
- title: "chore(deps): update Syft to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
- body: |
- Update Syft to ${{ steps.latest-version.outputs.LATEST_VERSION }}
- token: ${{ steps.generate-token.outputs.token }}
diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml
index edbe6ee72d5..75c3340bff5 100644
--- a/.github/workflows/validations.yaml
+++ b/.github/workflows/validations.yaml
@@ -14,9 +14,9 @@ jobs:
Static-Analysis:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Static analysis"
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -27,9 +27,9 @@ jobs:
Unit-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Unit tests"
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -40,9 +40,9 @@ jobs:
Quality-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Quality tests"
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04-4core-16gb
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: true
@@ -54,21 +54,56 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Archive the provider state
+ if: ${{ failure() }}
+ run: tar -czvf qg-capture-state.tar.gz -C test/quality --exclude tools --exclude labels .yardstick.yaml .yardstick
+
+ - name: Upload the provider state archive
+ if: ${{ failure() }}
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: qg-capture-state
+ path: qg-capture-state.tar.gz
+
+ - name: Show instructions to debug
+ if: ${{ failure() }}
+ run: |
+ ARCHIVE_BASENAME=qg-capture-state
+ ARCHIVE_NAME=$ARCHIVE_BASENAME.zip
+
+ cat << EOF >> $GITHUB_STEP_SUMMARY
+ ## Troubleshooting failed run
+
+ Download the artifact from this workflow run: \`$ARCHIVE_NAME\`
+
+ Then run the following commands to debug:
+ \`\`\`bash
+ # copy the archive to the tests/quality directory
+ cd test/quality
+ unzip $ARCHIVE_NAME && tar -xzf $ARCHIVE_BASENAME.tar.gz
+ \`\`\`
+
+ Now you can debug the with yardstick:
+ \`\`\`bash
+ poetry shell
+ yardstick result list
+ yardstick label explore
+ \`\`\`
+ EOF
+
+
Integration-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Integration tests"
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- - name: Validate grype output against the CycloneDX schema
- run: make validate-cyclonedx-schema
-
- name: Restore integration test cache
- uses: actions/cache@v3
+ 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') }}
@@ -78,9 +113,9 @@ jobs:
Build-Snapshot-Artifacts:
name: "Build snapshot artifacts"
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -99,28 +134,68 @@ 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@v3
+ 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)"
needs: [Build-Snapshot-Artifacts]
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Download snapshot build
- uses: actions/cache/restore@v3
+ 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@v3
+ 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') }}
@@ -142,17 +217,20 @@ jobs:
needs: [Build-Snapshot-Artifacts]
runs-on: macos-latest
steps:
- - uses: actions/checkout@v3
+ - name: Install Cosign
+ uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb #v3.8.2
+
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Download snapshot build
- uses: actions/cache/restore@v3
+ 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@v3
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3
with:
path: image.tar
key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }}
@@ -165,24 +243,43 @@ jobs:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "CLI tests (Linux)"
needs: [Build-Snapshot-Artifacts]
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Restore CLI test-fixture cache
- uses: actions/cache@v3
+ 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@v3
+ 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/.gitignore b/.gitignore
index 038a3818f29..f1e1f81bd2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,32 +1,72 @@
-/go.work
-/go.work.sum
+# local development tailoring
+go.work
+go.work.sum
+.tool-versions
-CHANGELOG.md
-VERSION
-/snapshot/
-/dist/
-*.profile
-.server
+# app configuration
+/.grype.yaml
+
+# tool and bin directories
+.tmp/
+bin/
+/bin
+/.bin
+/build
+/dist
+/snapshot
+/.tool
+/.task
+
+# changelog generation
+/CHANGELOG.md
+/VERSION
+
+# IDE configuration
+.vscode/
+.idea/
+.server/
+.history/
+
+# test related
*.fingerprint
+/test/results
+coverage.txt
+*.log
+.server
+
+# grype-db related
/metadata.json
/listing.json
-.vscode/
*.db
*.db-journal
!**/test-fixtures/**/*.db
+
+# probable archives
+.images
*.tar
-*tar.gz
-.idea/
+*.jar
+*.war
+*.ear
+*.jpi
+*.hpi
+*.zip
*.iml
-*.log
-.images
-.tmp/
-*.tmp
-coverage.txt
+
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, build with `go test -c`
+*.test
# OS files
.DS_Store
+*.profile
+
# Binaries for programs and plugins
*.exe
*.exe~
diff --git a/.gitmodules b/.gitmodules
index 281e4b3304f..21e606b75a5 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,4 +1,4 @@
[submodule "test/quality/vulnerability-match-labels"]
path = test/quality/vulnerability-match-labels
- url = git@github.com:anchore/vulnerability-match-labels.git
+ url = https://github.com/anchore/vulnerability-match-labels.git
branch = main
diff --git a/.golangci.yaml b/.golangci.yaml
index 184f0f8ec2d..68be2075aa3 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,58 +1,46 @@
-issues:
- max-same-issues: 25
-
- # 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
- - exportloopref
- 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
-output:
- uniq-by-line: false
-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".
@@ -80,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/.goreleaser.yaml b/.goreleaser.yaml
index 1d9552aa867..dd500a6ad2d 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -1,3 +1,5 @@
+version: 2
+
release:
prerelease: auto
draft: false
@@ -24,10 +26,10 @@ builds:
-w
-s
-extldflags '-static'
- -X github.com/anchore/grype/internal/version.version={{.Version}}
- -X github.com/anchore/grype/internal/version.gitCommit={{.Commit}}
- -X github.com/anchore/grype/internal/version.buildDate={{.Date}}
- -X github.com/anchore/grype/internal/version.gitDescription={{.Summary}}
+ -X main.version={{.Version}}
+ -X main.gitCommit={{.Commit}}
+ -X main.buildDate={{.Date}}
+ -X main.gitDescription={{.Summary}}
- id: darwin-build
dir: ./cmd/grype
@@ -41,7 +43,7 @@ builds:
ldflags: *build-ldflags
hooks:
post:
- - cmd: .tmp/quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv
+ - cmd: .tool/quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv
env:
- QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log
@@ -79,7 +81,7 @@ nfpms:
- deb
brews:
- - tap:
+ - repository:
owner: anchore
name: homebrew-grype
token: "{{.Env.GITHUB_BREW_TOKEN}}"
@@ -247,3 +249,16 @@ docker_manifests:
- ghcr.io/anchore/grype:{{.Tag}}-ppc64le
- ghcr.io/anchore/grype:{{.Tag}}-s390x
+
+signs:
+ - cmd: .tool/cosign
+ signature: "${artifact}.sig"
+ certificate: "${artifact}.pem"
+ args:
+ - "sign-blob"
+ - "--oidc-issuer=https://token.actions.githubusercontent.com"
+ - "--output-certificate=${certificate}"
+ - "--output-signature=${signature}"
+ - "${artifact}"
+ - "--yes"
+ artifacts: checksum
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000000..5d19a3c266d
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+[opensource@anchore.com](mailto:opensource@anchore.com).
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/Dockerfile b/Dockerfile
index 5eaab88c663..a02f87b98e5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,4 @@
-FROM gcr.io/distroless/static-debian11@sha256:5759d194607e472ff80fff5833442d3991dd89b219c96552837a2c8f74058617 AS build
-
+FROM gcr.io/distroless/static-debian12:latest AS build
FROM scratch
# needed for version check HTTPS request
diff --git a/Dockerfile.debug b/Dockerfile.debug
index db7a781d275..64c06a734e3 100644
--- a/Dockerfile.debug
+++ b/Dockerfile.debug
@@ -1,5 +1,4 @@
-FROM gcr.io/distroless/static-debian11:debug@sha256:c66a6ecb5aa7704a68c89d3ead1398adc7f16e214dda5f5f8e5d44351bcbf67d
-
+FROM gcr.io/distroless/static-debian12:debug
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
diff --git a/Makefile b/Makefile
index 9b7ced0d690..a048366a600 100644
--- a/Makefile
+++ b/Makefile
@@ -1,356 +1,43 @@
-BIN := grype
-TEMP_DIR := ./.tmp
-
-# Command templates #################################
-LINT_CMD := $(TEMP_DIR)/golangci-lint run --tests=false
-GOIMPORTS_CMD := $(TEMP_DIR)/gosimports -local github.com/anchore
-RELEASE_CMD := $(TEMP_DIR)/goreleaser release --clean
-SNAPSHOT_CMD := $(RELEASE_CMD) --skip-publish --skip-sign --snapshot
-CHRONICLE_CMD = $(TEMP_DIR)/chronicle
-GLOW_CMD = $(TEMP_DIR)/glow
-
-# Tool versions #################################
-GOLANGCILINT_VERSION := v1.53.3
-GOSIMPORTS_VERSION := v0.3.8
-BOUNCER_VERSION := v0.4.0
-CHRONICLE_VERSION := v0.6.0
-GORELEASER_VERSION := v1.19.2
-YAJSV_VERSION := v1.4.1
-QUILL_VERSION := v0.2.0
-GLOW_VERSION := v1.5.1
-SKOPEO_VERSION := v1.12.0
-
-# Formatting variables ############################
-BOLD := $(shell tput -T linux bold)
-PURPLE := $(shell tput -T linux setaf 5)
-GREEN := $(shell tput -T linux setaf 2)
-CYAN := $(shell tput -T linux setaf 6)
-RED := $(shell tput -T linux setaf 1)
-RESET := $(shell tput -T linux sgr0)
-TITLE := $(BOLD)$(PURPLE)
-SUCCESS := $(BOLD)$(GREEN)
-
-# Test variables #################################
-# the quality gate lower threshold for unit test total % coverage (by function statements)
-COVERAGE_THRESHOLD := 47
-RESULTS_DIR := $(TEMP_DIR)/results
-COVER_REPORT := $(RESULTS_DIR)/cover.report
-COVER_TOTAL := $(RESULTS_DIR)/cover.total
-LICENSES_REPORT := $(RESULTS_DIR)/licenses.json
-
-## Build variables #################################
-VERSION := $(shell git describe --dirty --always --tags)
-DIST_DIR := ./dist
-SNAPSHOT_DIR := ./snapshot
-CHANGELOG := CHANGELOG.md
-OS := $(shell uname | tr '[:upper:]' '[:lower:]')
-SNAPSHOT_BIN := $(realpath $(shell pwd)/$(SNAPSHOT_DIR)/$(OS)-build_$(OS)_amd64_v1/$(BIN))
-
-ifndef TEMP_DIR
- $(error TEMP_DIR is not set)
-endif
-
-ifndef RESULTS_DIR
- $(error RESULTS_DIR is not set)
-endif
-
-ifndef DIST_DIR
- $(error DIST_DIR is not set)
-endif
-
-ifndef SNAPSHOT_DIR
- $(error SNAPSHOT_DIR is not set)
-endif
-
-ifndef VERSION
- $(error VERSION is not set)
-endif
-
-define title
- @printf '$(TITLE)$(1)$(RESET)\n'
-endef
-
-define safe_rm_rf
- bash -c 'test -z "$(1)" && false || rm -rf $(1)'
-endef
-
-define safe_rm_rf_children
- bash -c 'test -z "$(1)" && false || rm -rf $(1)/*'
-endef
-
-.DEFAULT_GOAL:=help
-
-.PHONY: all
-all: static-analysis test ## Run all checks (linting, license check, unit, integration, and linux acceptance tests tests)
- @printf '$(SUCCESS)All checks pass!$(RESET)\n'
-
-.PHONY: static-analysis
-static-analysis: check-go-mod-tidy lint check-licenses validate-grype-db-schema
-
-.PHONY: test
-test: unit integration validate-cyclonedx-schema validate-grype-db-schema cli ## Run all tests (unit, integration, linux acceptance, and CLI tests)
-
-.PHONY: validate-cyclonedx-schema
-validate-cyclonedx-schema:
- cd schema/cyclonedx && make
-
-.PHONY: validate-grype-db-schema
-validate-grype-db-schema:
- # ensure the codebase is only referencing a single grype-db schema version, multiple is not allowed
- python test/validate-grype-db-schema.py
+TOOL_DIR = .tool
+BINNY = $(TOOL_DIR)/binny
+TASK = $(TOOL_DIR)/task
+.DEFAULT_GOAL := make-default
## Bootstrapping targets #################################
-.PHONY: bootstrap
-bootstrap: $(TEMP_DIR) bootstrap-go bootstrap-tools ## Download and install all tooling dependencies (+ prep tooling in the ./tmp dir)
- $(call title,Bootstrapping dependencies)
+# note: we need to assume that binny and task have not already been installed
+$(BINNY):
+ @mkdir -p $(TOOL_DIR)
+ @curl -sSfL https://raw.githubusercontent.com/anchore/binny/main/install.sh | sh -s -- -b $(TOOL_DIR)
-.PHONY: bootstrap-tools
-bootstrap-tools: $(TEMP_DIR)
- curl -sSfL https://raw.githubusercontent.com/anchore/quill/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(QUILL_VERSION)
- GO111MODULE=off GOBIN=$(realpath $(TEMP_DIR)) go get -u golang.org/x/perf/cmd/benchstat
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMP_DIR)/ $(GOLANGCILINT_VERSION)
- curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMP_DIR)/ $(BOUNCER_VERSION)
- curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(CHRONICLE_VERSION)
- .github/scripts/goreleaser-install.sh -d -b $(TEMP_DIR)/ $(GORELEASER_VERSION)
- # the only difference between goimports and gosimports is that gosimports removes extra whitespace between import blocks (see https://github.com/golang/go/issues/20818)
- GOBIN="$(realpath $(TEMP_DIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION)
- GOBIN="$(realpath $(TEMP_DIR))" go install github.com/neilpa/yajsv@$(YAJSV_VERSION)
- GOBIN="$(realpath $(TEMP_DIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION)
- GOBIN="$(realpath $(TEMP_DIR))" CGO_ENABLED=0 GO_DYN_FLAGS="" go install -tags "containers_image_openpgp" github.com/containers/skopeo/cmd/skopeo@$(SKOPEO_VERSION)
+# note: we need to assume that binny and task have not already been installed
+.PHONY: task
+$(TASK) task: $(BINNY)
+ @$(BINNY) install task -q
-.PHONY: bootstrap-go
-bootstrap-go:
+.PHONY: ci-bootstrap-go
+ci-bootstrap-go:
go mod download
-$(TEMP_DIR):
- mkdir -p $(TEMP_DIR)
-
-
-## Static analysis targets #################################
-
-.PHONY: lint
-lint: ## Run gofmt + golangci lint checks
- $(call title,Running linters)
- # ensure there are no go fmt differences
- @printf "files with gofmt issues: [$(shell gofmt -l -s .)]\n"
- @test -z "$(shell gofmt -l -s .)"
-
- # run all golangci-lint rules
- $(LINT_CMD)
- @[ -z "$(shell $(GOIMPORTS_CMD) -d .)" ] || (echo "goimports needs to be fixed" && false)
-
- # go tooling does not play well with certain filename characters, ensure the common cases don't result in future "go get" failures
- $(eval MALFORMED_FILENAMES := $(shell find . | grep -e ':'))
- @bash -c "[[ '$(MALFORMED_FILENAMES)' == '' ]] || (printf '\nfound unsupported filename characters:\n$(MALFORMED_FILENAMES)\n\n' && false)"
-
-.PHONY: format
-format: ## Auto-format all source code
- $(call title,Running formatters)
- gofmt -w -s .
- $(GOIMPORTS_CMD) -w .
- go mod tidy
-
-.PHONY: lint-fix
-lint-fix: format ## Auto-format all source code + run golangci lint fixers
- $(call title,Running lint fixers)
- $(LINT_CMD) --fix
-
-.PHONY: check-licenses
-check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
- $(call title,Checking for license compliance)
- $(TEMP_DIR)/bouncer check ./...
-
-check-go-mod-tidy:
- @ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
-
-## Testing targets #################################
-
-.PHONY: unit
-unit: $(TEMP_DIR) ## Run unit tests (with coverage)
- $(call title,Running unit tests)
- go test -coverprofile $(TEMP_DIR)/unit-coverage-details.txt $(shell go list ./... | grep -v anchore/grype/test)
- @.github/scripts/coverage.py $(COVERAGE_THRESHOLD) $(TEMP_DIR)/unit-coverage-details.txt
-
-.PHONY: integration
-integration: ## Run integration tests
- $(call title,Running integration tests)
- go test -v ./test/integration
-
-.PHONY: quality
-quality: ## Run quality tests
- $(call title,Running quality tests)
- cd test/quality && make
-
-.PHONY: cli
-cli: $(SNAPSHOT_DIR) ## Run CLI tests
- chmod 755 "$(SNAPSHOT_BIN)"
- $(SNAPSHOT_BIN) version
- SYFT_BINARY_LOCATION='$(SNAPSHOT_BIN)' \
- go test -count=1 -timeout=15m -v ./test/cli
-
-## Test-fixture-related targets #################################
-
-# note: this is used by CI to determine if various test fixture cache should be restored or recreated
-# TODO (cphillips) check for all fixtures and individual makefile
-fingerprints:
- $(call title,Creating all test cache input fingerprints)
-
- # for IMAGE integration test fixtures
- cd test/integration/test-fixtures && \
- make cache.fingerprint
-
- # for INSTALL integration test fixtures
- cd test/install && \
- make cache.fingerprint
-
- # for CLI test fixtures
- cd test/cli/test-fixtures && \
- make cache.fingerprint
-
-.PHONY: show-test-image-cache
-show-test-image-cache: ## Show all docker and image tar cache
- $(call title,Docker daemon cache)
- @docker images --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep stereoscope-fixture- | sort
-
- $(call title,Tar cache)
- @find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" | sort
-
-.PHONY: show-test-snapshots
-show-test-snapshots: ## Show all test snapshots
- $(call title,Test snapshots)
- @find . -type f -wholename "**/test-fixtures/snapshot/*" | sort
-
-## install.sh testing targets #################################
-
-install-test: $(SNAPSHOT_DIR)
- cd test/install && \
- make
-
-install-test-cache-save: $(SNAPSHOT_DIR)
- cd test/install && \
- make save
-
-install-test-cache-load: $(SNAPSHOT_DIR)
- cd test/install && \
- make load
-
-install-test-ci-mac: $(SNAPSHOT_DIR)
- cd test/install && \
- make ci-test-mac
-
-.PHONY: compare-test-deb-package-install
-compare-test-deb-package-install: $(TEMP_DIR) $(SNAPSHOT_DIR)
- $(call title,Running compare test: DEB install)
- $(COMPARE_DIR)/deb.sh \
- $(SNAPSHOT_DIR) \
- $(COMPARE_DIR) \
- $(COMPARE_TEST_IMAGE) \
- $(TEMP_DIR)
-
-.PHONY: compare-test-rpm-package-install
-compare-test-rpm-package-install: $(TEMP_DIR) $(SNAPSHOT_DIR)
- $(call title,Running compare test: RPM install)
- $(COMPARE_DIR)/rpm.sh \
- $(SNAPSHOT_DIR) \
- $(COMPARE_DIR) \
- $(COMPARE_TEST_IMAGE) \
- $(TEMP_DIR)
-
-## Code generation targets #################################
-## TODO (cphillips) what does grype have here?
-
-## Build-related targets #################################
-
-.PHONY: build
-build: $(SNAPSHOT_DIR)
-
-$(SNAPSHOT_DIR): ## Build snapshot release binaries and packages
- $(call title,Building snapshot artifacts)
-
- # create a config with the dist dir overridden
- echo "dist: $(SNAPSHOT_DIR)" > $(TEMP_DIR)/goreleaser.yaml
- cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml
-
- # build release snapshots
- $(SNAPSHOT_CMD) --config $(TEMP_DIR)/goreleaser.yaml
-
-.PHONY: changelog
-changelog: clean-changelog ## Generate and show the changelog for the current unreleased version
- $(CHRONICLE_CMD) -vv -n --version-file VERSION > $(CHANGELOG)
- @$(GLOW_CMD) $(CHANGELOG)
-
-$(CHANGELOG):
- $(CHRONICLE_CMD) -vvv > $(CHANGELOG)
-
-.PHONY: release
-release:
- @.github/scripts/trigger-release.sh
-
-.PHONY: ci-release
-ci-release: ci-check clean-dist $(CHANGELOG)
- $(call title,Publishing release artifacts)
-
- # create a config with the dist dir overridden
- echo "dist: $(DIST_DIR)" > $(TEMP_DIR)/goreleaser.yaml
- cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml
-
- bash -c "\
- $(RELEASE_CMD) \
- --config $(TEMP_DIR)/goreleaser.yaml \
- --release-notes <(cat $(CHANGELOG)) \
- || (cat /tmp/quill-*.log && false)"
-
- # upload the version file that supports the application version update check (excluding pre-releases)
- .github/scripts/update-version-file.sh "$(DIST_DIR)" "$(VERSION)"
-
-.PHONY: ci-check
-ci-check:
- @.github/scripts/ci-check.sh
-
-## Cleanup targets #################################
-
-.PHONY: clean
-clean: clean-dist clean-snapshot ## Remove previous builds, result reports, and test cache
- $(call safe_rm_rf_children,$(TEMP_DIR))
-
-.PHONY: clean-snapshot
-clean-snapshot:
- $(call safe_rm_rf,$(SNAPSHOT_DIR))
- rm -f $(TEMP_DIR)/goreleaser.yaml
-
-.PHONY: clean-dist
-clean-dist: clean-changelog
- $(call safe_rm_rf,$(DIST_DIR))
- rm -f $(TEMP_DIR)/goreleaser.yaml
-
-.PHONY: clean-changelog
-clean-changelog:
- rm -f $(CHANGELOG) VERSION
-
-clean-test-image-cache: clean-test-image-tar-cache clean-test-image-docker-cache ## Clean test image cache
-
-## Halp! #################################
+# this is a bootstrapping catch-all, where if the target doesn't exist, we'll ensure the tools are installed and then try again
+%:
+ make $(TASK)
+ $(TASK) $@
-.PHONY: help
-help: ## Display this help
- @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
+## Shim targets #################################
+.PHONY: make-default
+make-default: $(TASK)
+ @# run the default task in the taskfile
+ @$(TASK)
-.PHONY: validate-grype-test-config
-validate-grype-test-config:
- # ensure the update URL is not overridden (not pointing to staging)
- @bash -c '\
- grep -q "update-url" test/grype-test-config.yaml; \
- if [ $$? -eq 0 ]; then \
- echo "Found \"update-url\" in CLI testing config. Cannot release if previous CLI testing did not use production (default) values"; \
- fi'
+# for those of us that can't seem to kick the habit of typing `make ...` lets wrap the superior `task` tool
+TASKS := $(shell bash -c "test -f $(TASK) && $(TASK) -l | grep '^\* ' | cut -d' ' -f2 | tr -d ':' | tr '\n' ' '" ) $(shell bash -c "test -f $(TASK) && $(TASK) -l | grep 'aliases:' | cut -d ':' -f 3 | tr '\n' ' ' | tr -d ','")
-.PHONY: validate-syft-release-version
-validate-syft-release-version:
- @./.github/scripts/syft-released-version-check.sh
+.PHONY: $(TASKS)
+$(TASKS): $(TASK)
+ @$(TASK) $@
-.PHONY: clean-test-cache
-clean-test-cache: ## Delete all test cache (built docker image tars)
- find . -type f -wholename "**/test-fixtures/cache/*.tar" -delete
+help: $(TASK)
+ @$(TASK) -l
diff --git a/README.md b/README.md
index a59345e47a7..c7ce525c21d 100644
--- a/README.md
+++ b/README.md
@@ -2,14 +2,19 @@
-[](https://github.com/anchore/grype/actions?query=workflow%3A%22Static+Analysis+%2B+Unit+%2B+Integration%22)
-[](https://github.com/anchore/grype/actions?query=workflow%3AAcceptance)
-[](https://goreportcard.com/report/github.com/anchore/grype)
-[](https://github.com/anchore/grype/releases/latest)
-[](https://github.com/anchore/grype)
-[](https://github.com/anchore/grype/blob/main/LICENSE)
-[](https://anchore.com/slack)
-[](https://api.securityscorecards.dev/projects/github.com/anchore/grype)
+
+
+
+
+
+
+
+
+
+
+
+
+
A vulnerability scanner for container images and filesystems. Easily [install the binary](#installation) to try it out. Works with [Syft](https://github.com/anchore/syft), the powerful SBOM (software bill of materials) tool for container images and filesystems.
@@ -19,7 +24,7 @@ A vulnerability scanner for container images and filesystems. Easily [install th
- Agenda: https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing (join [this group](https://groups.google.com/g/anchore-oss-community) for write access)
- All are welcome!
-For commercial support options with Syft or Grype, please [contact Anchore](https://get.anchore.com/contact/)
+For commercial support options with Syft or Grype, please [contact Anchore](https://get.anchore.com/contact/).

@@ -29,6 +34,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
- Find vulnerabilities for major operating system packages:
- Alpine
- Amazon Linux
+ - Azure Linux (previously CBL-Mariner)
- BusyBox
- CentOS
- Debian
@@ -36,6 +42,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
- Oracle Linux
- Red Hat (RHEL)
- Ubuntu
+ - Wolfi
- Find vulnerabilities for language-specific packages:
- Ruby (Gems)
- Java (JAR, WAR, EAR, JPI, HPI)
@@ -46,6 +53,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
- PHP (Composer)
- Rust (Cargo)
- Supports Docker, OCI and [Singularity](https://github.com/sylabs/singularity) image formats.
+- [OpenVEX](https://github.com/openvex) support for filtering and augmenting scanning results.
If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/grype/issues).
@@ -56,11 +64,17 @@ If you encounter an issue, please [let us know using the issue tracker](https://
```bash
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
```
+Install script options:
+- `-b`: Specify a custom installation directory (defaults to `./bin`)
+- `-d`: More verbose logging levels (`-d` for debug, `-dd` for trace)
+- `-v`: Verify the signature of the downloaded artifact before installation (requires [`cosign`](https://github.com/sigstore/cosign) to be installed)
-You can also choose another destination directory and release version for the installation. The destination directory doesn't need to be `/usr/local/bin`, it just needs to be a location found in the user's PATH and writable by the user that's installing Grype.
+### Chocolatey
-```
-curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b
+The chocolatey distribution of grype is community-maintained and not distributed by the anchore team.
+
+```bash
+choco install grype -y
```
### Homebrew
@@ -72,7 +86,7 @@ brew install grype
### MacPorts
-On macOS, Grype can additionally be installed from the [community maintained port](https://ports.macports.org/port/grype/) via MacPorts:
+On macOS, Grype can additionally be installed from the [community-maintained port](https://ports.macports.org/port/grype/) via MacPorts:
```bash
sudo port install grype
@@ -86,7 +100,35 @@ See [DEVELOPING.md](DEVELOPING.md#native-development) for instructions to build
### GitHub Actions
-If you're using GitHub Actions, you can simply use our [Grype-based action](https://github.com/marketplace/actions/anchore-container-scan) to run vulnerability scans on your code or container images during your CI workflows.
+If you're using GitHub Actions, you can use our [Grype-based action](https://github.com/marketplace/actions/anchore-container-scan) to run vulnerability scans on your code or container images during your CI workflows.
+
+## Verifying the artifacts
+
+Checksums are applied to all artifacts, and the resulting checksum file is signed using cosign.
+
+You need the following tool to verify signature:
+
+- [Cosign](https://docs.sigstore.dev/cosign/system_config/installation/)
+
+Verification steps are as follow:
+
+1. Download the files you want, and the checksums.txt, checksums.txt.pem and checksums.txt.sig files from the [releases](https://github.com/anchore/grype/releases) page:
+
+2. Verify the signature:
+
+```shell
+cosign verify-blob \
+--certificate \
+--signature \
+--certificate-identity-regexp 'https://github\.com/anchore/grype/\.github/workflows/.+' \
+--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
+```
+
+3. Once the signature is confirmed as valid, you can proceed to validate that the SHA256 sums align with the downloaded artifact:
+
+```shell
+sha256sum --ignore-missing -c checksums.txt
+```
## Getting started
@@ -96,7 +138,7 @@ If you're using GitHub Actions, you can simply use our [Grype-based action](http
grype
```
-The above command scans for vulnerabilities that are visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the vulnerability scan, regardless of its presence in the final image, provide `--scope all-layers`:
+The above command scans for vulnerabilities visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the vulnerability scan, regardless of its presence in the final image, provide `--scope all-layers`:
```
grype --scope all-layers
@@ -111,7 +153,7 @@ docker run --rm \
$(ImageName):$(ImageTag)
```
-### Supported sources
+## Supported sources
Grype can scan a variety of sources beyond those found in Docker.
@@ -136,6 +178,7 @@ oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
dir:path/to/yourproject read directly from a path on disk (any directory)
+file:path/to/yourfile read directly from a file on disk
sbom:path/to/syft.json read Syft JSON from path on disk
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
```
@@ -159,14 +202,70 @@ cat ./sbom.json | grype
Grype supports input of [Syft](https://github.com/anchore/syft), [SPDX](https://spdx.dev/), and [CycloneDX](https://cyclonedx.org/)
SBOM formats. If Syft has generated any of these file types, they should have the appropriate information to work properly with Grype.
It is also possible to use SBOMs generated by other tools with varying degrees of success. Two things that make Grype matching
-more successful are inclusion of CPE and Linux distribution information. If an SBOM does not include any CPE information, it
+more successful are the inclusion of CPE and Linux distribution information. If an SBOM does not include any CPE information, it
is possible to generate these based on package information using the `--add-cpes-if-none` flag. To specify a distribution,
use the `--distro :` flag. A full example is:
```
-grype --add-cpes-if-none --distro alpine:3.10 sbom:some-apline-3.10.spdx.json
+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
+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.
+
+Only the latest database schema is considered to be supported. When a new database schema is introduced then the one it replaces is
+marked as deprecated. Deprecated schemas will continue to receive updates for at least one year after they are marked
+as deprecated at which point they will no longer be supported.
+
### Working with attestations
Grype supports scanning SBOMs as input via stdin. Users can use [cosign](https://github.com/sigstore/cosign) to verify attestations
with an SBOM as its content to scan an image for vulnerabilities:
@@ -235,10 +334,13 @@ external-sources:
maven:
search-upstream-by-sha1: true
base-url: https://search.maven.org/solrsearch/select
+ rate-limit: 300ms # Time between Maven API requests
```
You can also configure the base-url if you're using another registry as your maven endpoint.
+The rate at which Maven API requests are made can be configured to match your environment's requirements. The default is 300ms between requests.
+
### Output formats
The output format for Grype is configurable as well:
@@ -250,9 +352,10 @@ grype -o
Where the formats available are:
- `table`: A columnar summary (default).
-- `cyclonedx`: An XML report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
-- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
+- `cyclonedx`: An XML report conforming to the [CycloneDX 1.6 specification](https://cyclonedx.org/specification/overview/).
+- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.6 specification](https://cyclonedx.org/specification/overview/).
- `json`: Use this to get as much information out of Grype as possible!
+- `sarif`: Use this option to get a [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) report (Static Analysis Results Interchange Format)
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
### Using templates
@@ -269,7 +372,7 @@ Grype lets you define custom output formats, using [Go templates](https://golang
**Please note:** Templates can access information about the system they are running on, such as environment variables. You should never run untrusted templates.
-There are several example templates in the [templates](https://github.com/anchore/grype/tree/main/templates) directory in the Grype source which can serve a starting point for a custom output format. For example, [csv.tmpl](https://github.com/anchore/grype/blob/main/templates/csv.tmpl) produces a vulnerability report in CSV (comma separated value) format:
+There are several example templates in the [templates](https://github.com/anchore/grype/tree/main/templates) directory in the Grype source which can serve as a starting point for a custom output format. For example, [csv.tmpl](https://github.com/anchore/grype/blob/main/templates/csv.tmpl) produces a vulnerability report in CSV (comma separated value) format:
```text
"Package","Version Installed","Vulnerability ID","Severity"
@@ -293,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.
@@ -315,6 +420,9 @@ ignore:
# This is the full set of supported rule fields:
- vulnerability: CVE-2008-4318
fix-state: unknown
+ # VEX fields apply when Grype reads vex data:
+ vex-status: not_affected
+ vex-justification: vulnerable_code_not_present
package:
name: libcurl
version: 1.5.1
@@ -361,7 +469,79 @@ NAME INSTALLED FIXED-IN VULNERABILITY SEVERITY
apk-tools 2.10.6-r0 2.10.7-r0 CVE-2021-36159 Critical
```
-If you want Grype to only report vulnerabilities **that do not have a confirmed fix**, you can use the `--only-notfixed` flag. (This automatically adds [ignore rules](#specifying-matches-to-ignore) into Grype's configuration, such that vulnerabilities that are fixed will be ignored.)
+If you want Grype to only report vulnerabilities **that do not have a confirmed fix**, you can use the `--only-notfixed` flag. Alternatively, you can use the `--ignore-states` flag to filter results for vulnerabilities with specific states such as `wont-fix` (see `--help` for a list of valid fix states). These flags automatically add [ignore rules](#specifying-matches-to-ignore) into Grype's configuration, such that vulnerabilities which are fixed, or will not be fixed, will be ignored.
+
+## VEX Support
+
+Grype can use VEX (Vulnerability Exploitability Exchange) data to filter false
+positives or provide additional context, augmenting matches. When scanning a
+container image, you can use the `--vex` flag to point to one or more
+[OpenVEX](https://github.com/openvex) documents.
+
+VEX statements relate a product (a container image), a vulnerability, and a VEX
+status to express an assertion of the vulnerability's impact. There are four
+[VEX statuses](https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-labels):
+`not_affected`, `affected`, `fixed` and `under_investigation`.
+
+Here is an example of a simple OpenVEX document. (tip: use
+[`vexctl`](https://github.com/openvex/vexctl) to generate your own documents).
+
+```json
+{
+ "@context": "https://openvex.dev/ns/v0.2.0",
+ "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
+ "author": "A Grype User ",
+ "timestamp": "2023-07-17T18:28:47.696004345-06:00",
+ "version": 1,
+ "statements": [
+ {
+ "vulnerability": {
+ "name": "CVE-2023-1255"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ "subcomponents": [
+ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" },
+ { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" }
+ ]
+ }
+ ],
+ "status": "fixed"
+ }
+ ]
+}
+```
+
+By default, Grype will use any statements in specified VEX documents with a
+status of `not_affected` or `fixed` to move matches to the ignore set.
+
+Any matches ignored as a result of VEX statements are flagged when using
+`--show-suppressed`:
+
+```
+libcrypto3 3.0.8-r3 3.0.8-r4 apk CVE-2023-1255 Medium (suppressed by VEX)
+```
+
+Statements with an `affected` or `under_investigation` status will only be
+considered to augment the result set when specifically requested using the
+`GRYPE_VEX_ADD` environment variable or in a configuration file.
+
+
+### VEX Ignore Rules
+
+Ignore rules can be written to control how Grype honors VEX statements. For
+example, to configure Grype to only act on VEX statements when the justification is `vulnerable_code_not_present`, you can write a rule like this:
+
+```yaml
+---
+ignore:
+ - vex-status: not_affected
+ vex-justification: vulnerable_code_not_present
+```
+
+See the [list of justifications](https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-justifications) for details. You can mix `vex-status` and `vex-justification`
+with other ignore rule parameters.
## Grype's database
@@ -369,14 +549,16 @@ When Grype performs a scan for vulnerabilities, it does so using a vulnerability
- Alpine Linux SecDB: https://secdb.alpinelinux.org/
- Amazon Linux ALAS: https://alas.aws.amazon.com/AL2/alas.rss
-- RedHat RHSAs: https://www.redhat.com/security/data/oval/
+- Chainguard SecDB: https://packages.cgr.dev/chainguard/security.json
- Debian Linux CVE Tracker: https://security-tracker.debian.org/tracker/data/json
-- Github GHSAs: https://github.com/advisories
+- GitHub Security Advisories (GHSAs): https://github.com/advisories
- National Vulnerability Database (NVD): https://nvd.nist.gov/vuln/data-feeds
- Oracle Linux OVAL: https://linux.oracle.com/security/oval/
- RedHat Linux Security Data: https://access.redhat.com/hydra/rest/securitydata/
-- Suse Linux OVAL: https://ftp.suse.com/pub/projects/security/oval/
+- RedHat RHSAs: https://www.redhat.com/security/data/oval/
+- SUSE Linux OVAL: https://ftp.suse.com/pub/projects/security/oval/
- Ubuntu Linux Security: https://people.canonical.com/~ubuntu-security/
+- Wolfi SecDB: https://packages.wolfi.dev/os/security.json
By default, Grype automatically manages this database for you. Grype checks for new updates to the vulnerability database to make sure that every scan uses up-to-date vulnerability information. This behavior is configurable. For more information, see the [Managing Grype's database](#managing-grypes-database) section.
@@ -384,24 +566,25 @@ By default, Grype automatically manages this database for you. Grype checks for
Grype's vulnerability database is a SQLite file, named `vulnerability.db`. Updates to the database are atomic: the entire database is replaced and then treated as "readonly" by Grype.
-Grype's first step in a database update is discovering databases that are available for retrieval. Grype does this by requesting a "listing file" from a public endpoint:
+Grype's first step in a database update is discovering databases that are available for retrieval. Grype does this by requesting a "latest database file" from a public endpoint:
-`https://toolbox-data.anchore.io/grype/databases/listing.json`
+https://grype.anchore.io/databases/v6/latest.json
-The listing file contains entries for every database that's available for download.
+The latest database file contains an entry for the most recent database available for download.
-Here's an example of an entry in the listing file:
+Here's an example of an entry in the latest database file:
```json
{
- "built": "2021-10-21T08:13:41Z",
- "version": 3,
- "url": "https://toolbox-data.anchore.io/grype/databases/vulnerability-db_v3_2021-10-21T08:13:41Z.tar.gz",
- "checksum": "sha256:8c99fb4e516f10b304f026267c2a73a474e2df878a59bf688cfb0f094bfe7a91"
+ "status": "active",
+ "schemaVersion": "6.0.0",
+ "built": "2025-02-11T04:06:41Z",
+ "path": "vulnerability-db_v6.0.0_2025-02-11T01:30:51Z_1739246801.tar.zst",
+ "checksum": "sha256:79bfa04265c5a32d21773ad0da1bda13c31e932fa1e1422db635c8d714038868"
}
```
-With this information, Grype can select the correct database (the most recently built database with the current schema version), download the database, and verify the database's integrity using the listed `checksum` value.
+With this information, Grype can find the most recently built database with the current schema version, download the database, and verify the database's integrity using the `checksum` value.
### Managing Grype's database
@@ -409,9 +592,9 @@ With this information, Grype can select the correct database (the most recently
#### Local database cache directory
-By default, the database is cached on the local filesystem in the directory `$XDG_CACHE_HOME/grype/db//`. For example, on macOS, the database would be stored in `~/Library/Caches/grype/db/3/`. (For more information on XDG paths, refer to the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).)
+By default, the database is cached on the local filesystem in the directory `$XDG_CACHE_HOME/grype/db//`. For example, on macOS, the database would be stored in `~/Library/Caches/grype/db/6/`. (For more information on XDG paths, refer to the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).)
-You can set the cache directory path using the environment variable `GRYPE_DB_CACHE_DIR`.
+You can set the cache directory path using the environment variable `GRYPE_DB_CACHE_DIR`. If setting that variable alone does not work, then the `TMPDIR` environment variable might also need to be set.
#### Data staleness
@@ -419,11 +602,13 @@ Grype needs up-to-date vulnerability information to provide accurate matches. By
#### Offline and air-gapped environments
-By default, Grype checks for a new database on every run, by making a network call over the Internet. You can tell Grype not to perform this check by setting the environment variable `GRYPE_DB_AUTO_UPDATE` to `false`.
+By default, Grype checks for a new database on every run, by making a network request over the internet.
+You can tell Grype not to perform this check by setting the environment variable `GRYPE_DB_AUTO_UPDATE` to `false`.
-As long as you place Grype's `vulnerability.db` and `metadata.json` files in the cache directory for the expected schema version, Grype has no need to access the network. Additionally, you can get a listing of the database archives available for download from the `grype db list` command in an online environment, download the database archive, transfer it to your offline environment, and use `grype db import ` to use the given database in an offline capacity.
+As long as you place Grype's `vulnerability.db` and `import.json` files in the cache directory for the expected schema version, Grype has no need to access the network.
+Additionally, you can get a reference to the latest database archive for download from the `grype db list` command in an online environment, download the database archive, transfer it to your offline environment, and use `grype db import ` to use the given database in an offline capacity.
-If you would like to distribute your own Grype databases internally without needing to use `db import` manually you can leverage Grype's DB update mechanism. To do this you can craft your own `listing.json` file similar to the one found publically (see `grype db list -o raw` for an example of our public `listing.json` file) and change the download URL to point to an internal endpoint (e.g. a private S3 bucket, an internal file server, etc). Any internal installation of Grype can receive database updates automatically by configuring the `db.update-url` (same as the `GRYPE_DB_UPDATE_URL` environment variable) to point to the hosted `listing.json` file you've crafted.
+If you would like to distribute your own Grype databases internally without needing to use `db import` manually you can leverage Grype's DB update mechanism. To do this you can craft your own `latest.json` file similar to the public "latest database file" and change the download URL to point to an internal endpoint (e.g. a private S3 bucket, an internal file server, etc.). Any internal installation of Grype can receive database updates automatically by configuring the `db.update-url` (same as the `GRYPE_DB_UPDATE_URL` environment variable) to point to the hosted `latest.json` file you've crafted.
#### CLI commands for database management
@@ -435,10 +620,12 @@ Grype provides database-specific CLI commands for users that want to control the
`grype db update` — ensure the latest database has been downloaded to the cache directory (Grype performs this operation at the beginning of every scan by default)
-`grype db list` — download the listing file configured at `db.update-url` and show databases that are available for download
+`grype db list` — download the latest database file configured at `db.update-url` and show the database available for download
`grype db import` — provide grype with a database archive to explicitly use (useful for offline DB updates)
+`grype db providers` - provides a detailed list of database providers
+
Find complete information on Grype's database commands by running `grype db --help`.
## Shell completion
@@ -464,12 +651,12 @@ An example `config.json` looks something like this:
```
// config.json
{
- "auths": {
- "registry.example.com": {
- "username": "AzureDiamond",
- "password": "hunter2"
- }
- }
+ "auths": {
+ "registry.example.com": {
+ "username": "AzureDiamond",
+ "password": "hunter2"
+ }
+ }
}
```
@@ -533,167 +720,262 @@ They will also not be dependent on a docker daemon, (or some other runtime softw
## Configuration
-Default configuration search paths:
+Default configuration search paths (see all with `grype config locations`):
- `.grype.yaml`
- `.grype/config.yaml`
- `~/.grype.yaml`
- `/grype/config.yaml`
-You can also use the `--config` / `-c` flag to provide your own configuration file/path:
+Use `grype config` to print a sample config file to stdout.
+Use `grype config --load` to print the current config after loading all values to stdout.
-```
+You can specify files directly using the `--config` / `-c` flags (or environment variable `GRYPE_CONFIG`) to provide your own configuration files/paths:
+
+```shell
+# Using the flag
grype -c /path/to/config.yaml
+# Or using the environment variable
+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
-check-for-app-update: true
+# 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'
-# 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: ""
+# 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: ''
-# same as --name; set the name of the target being analyzed
-name: ""
+# pretty-print JSON output (env: GRYPE_PRETTY)
+pretty: false
-# 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: ""
+# distro to match against in the format: : (env: GRYPE_DISTRO)
+distro: ''
-# the output format of the vulnerability report (options: table, json, cyclonedx)
-# same as -o ; GRYPE_OUTPUT env var
-output: "table"
+# generate CPEs for packages with no CPE data (env: GRYPE_ADD_CPES_IF_NONE)
+add-cpes-if-none: false
-# suppress all output (except for the vulnerability list)
-# same as -q ; GRYPE_QUIET env var
-quiet: false
+# specify the path to a Go template file (requires 'template' output to be selected) (env: GRYPE_OUTPUT_TEMPLATE_FILE)
+output-template-file: ''
-# write output report to a file (default is to write to stdout)
-# same as --file; GRYPE_FILE env var
-file: ""
+# enable/disable checking for application updates on startup (env: GRYPE_CHECK_FOR_APP_UPDATE)
+check-for-app-update: true
-# a list of globs to exclude from scanning, for example:
-# exclude:
-# - '/etc/**'
-# - './out/**/*.json'
-# same as --exclude ; GRYPE_EXCLUDE env var
-exclude: []
+# ignore matches for vulnerabilities that are not fixed (env: GRYPE_ONLY_FIXED)
+only-fixed: false
-# os and/or architecture to use when referencing container images (e.g. "windows/armv6" or "arm64")
-# same as --platform; GRYPE_PLATFORM env var
-platform: ""
+# ignore matches for vulnerabilities that are fixed (env: GRYPE_ONLY_NOTFIXED)
+only-notfixed: false
-# If using SBOM input, automatically generate CPEs when packages have none
-add-cpes-if-none: false
+# ignore matches for vulnerabilities with specified comma separated fix states, options=[fixed not-fixed unknown wont-fix] (env: GRYPE_IGNORE_WONTFIX)
+ignore-wontfix: ''
-# Explicitly specify a linux distribution to use as : like alpine:3.10
-distro:
+# an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux') (env: GRYPE_PLATFORM)
+platform: ''
-external-sources:
- enable: false
- maven:
- search-upstream-by-sha1: true
- base-url: https://search.maven.org/solrsearch/select
+# 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) (env: GRYPE_FAIL_ON_SEVERITY)
+fail-on-severity: ''
-db:
- # check for database updates on execution
- # same as GRYPE_DB_AUTO_UPDATE env var
- auto-update: true
+# show suppressed/ignored vulnerabilities in the output (only supported with table output format) (env: GRYPE_SHOW_SUPPRESSED)
+show-suppressed: false
- # location to write the vulnerability database cache
- # same as GRYPE_DB_CACHE_DIR env var
- cache-dir: "$XDG_CACHE_HOME/grype/db"
+# orient results by CVE instead of the original vulnerability ID when possible (env: GRYPE_BY_CVE)
+by-cve: false
- # URL of the vulnerability database
- # same as GRYPE_DB_UPDATE_URL env var
- update-url: "https://toolbox-data.anchore.io/grype/databases/listing.json"
+# sort the match results with the given strategy, options=[package severity epss risk kev vulnerability] (env: GRYPE_SORT_BY)
+sort-by: 'risk'
- # it ensures db build is no older than the max-allowed-built-age
- # set to false to disable check
- validate-age: true
+# same as --name; set the name of the target being analyzed (env: GRYPE_NAME)
+name: ''
- # 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"
+# 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: ''
search:
- # the search space to look for packages (options: all-layers, squashed)
- # same as -s ; GRYPE_SEARCH_SCOPE env var
- scope: "squashed"
-
- # 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
+ # selection of layers to analyze, options=[squashed all-layers] (env: GRYPE_SEARCH_SCOPE)
+ scope: 'squashed'
# 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
+ # note: for now this only applies to the java package cataloger (env: GRYPE_SEARCH_UNINDEXED_ARCHIVES)
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
- insecure-skip-tls-verify: false
- # use http instead of https when connecting to the registry
- # same as GRYPE_REGISTRY_INSECURE_USE_HTTP env var
- insecure-use-http: 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
- # credentials for specific registries
- auth:
- - # the URL to the registry (e.g. "docker.io", "localhost:5000", etc.)
- # same as GRYPE_REGISTRY_AUTH_AUTHORITY env var
- authority: ""
- # same as GRYPE_REGISTRY_AUTH_USERNAME env var
- username: ""
- # same as GRYPE_REGISTRY_AUTH_PASSWORD env var
- password: ""
- # note: token and username/password are mutually exclusive
- # same as GRYPE_REGISTRY_AUTH_TOKEN env var
- token: ""
- - ... # note, more credentials can be provided via config file only
+# 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: []
-log:
- # use structured logging
- # same as GRYPE_LOG_STRUCTURED env var
- structured: false
+# a list of globs to exclude from scanning, for example:
+# - '/etc/**'
+# - './out/**/*.json'
+# same as --exclude (env: GRYPE_EXCLUDE)
+exclude: []
- # 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"
+external-sources:
+ # enable Grype searching network source for additional information (env: GRYPE_EXTERNAL_SOURCES_ENABLE)
+ enable: false
+
+ maven:
+ # search for Maven artifacts by SHA1 (env: GRYPE_EXTERNAL_SOURCES_MAVEN_SEARCH_MAVEN_UPSTREAM)
+ search-maven-upstream: true
- # location to write the log file (default is not to have a log file)
- # same as GRYPE_LOG_FILE env var
- file: ""
+ # 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
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:
+ # 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
- python:
- using-cpes: true
- javascript:
- using-cpes: true
- ruby:
- using-cpes: true
+
dotnet:
- using-cpes: true
+ # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_DOTNET_USING_CPES)
+ using-cpes: false
+
golang:
- using-cpes: true
+ # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_GOLANG_USING_CPES)
+ using-cpes: false
+
+ # 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
+
+ # 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
+
+ javascript:
+ # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_JAVASCRIPT_USING_CPES)
+ using-cpes: false
+
+ python:
+ # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_PYTHON_USING_CPES)
+ using-cpes: false
+
+ ruby:
+ # use CPE matching to find vulnerabilities (env: GRYPE_MATCH_RUBY_USING_CPES)
+ using-cpes: false
+
+ 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
+
+
+registry:
+ # 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 (env: GRYPE_REGISTRY_INSECURE_USE_HTTP)
+ insecure-use-http: false
+
+ # 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: []
+
+# VEX statuses to consider as ignored rules (env: GRYPE_VEX_ADD)
+vex-add: []
+
+# 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'
+
+ # certificate to trust download the database and listing file (env: GRYPE_DB_CA_CERT)
+ ca-cert: ''
+
+ # check for database updates on execution (env: GRYPE_DB_AUTO_UPDATE)
+ auto-update: true
+
+ # validate the database matches the known hash each execution (env: GRYPE_DB_VALIDATE_BY_HASH_ON_START)
+ validate-by-hash-on-start: true
+
+ # ensure db build is no older than the max-allowed-built-age (env: GRYPE_DB_VALIDATE_AGE)
+ validate-age: true
+
+ # 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 logging output (env: GRYPE_LOG_QUIET)
+ quiet: false
+
+ # explicitly set the logging level (available: [error warn info debug trace]) (env: GRYPE_LOG_LEVEL)
+ level: 'warn'
+
+ # file path to write logs to (env: GRYPE_LOG_FILE)
+ file: ''
+
+dev:
+ # capture resource profiling data (available: [cpu, mem]) (env: GRYPE_DEV_PROFILE)
+ profile: ''
+
+ db:
+ # show sql queries in trace logging (requires -vv) (env: GRYPE_DEV_DB_DEBUG)
+ debug: false
```
## Future plans
@@ -701,3 +983,8 @@ match:
The following areas of potential development are currently being investigated:
- Support for allowlist, package mapping
+
+
+## Grype Logo
+
+Grype Logo by Anchore is licensed under CC BY 4.0
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 00000000000..7055f92d788
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,39 @@
+# Release
+
+A release of grype comprises:
+- a new semver git tag from the current tip of the main branch
+- a new [github release](https://github.com/anchore/grype/releases) with a changelog and archived binary assets
+- docker images published to `ghcr.io` and `dockerhub`, including multi architecture images + manifest
+- [`anchore/homebrew-grype`](https://github.com/anchore/homebrew-grype) tap updated to point to assets in the latest github release
+
+Ideally releasing should be done often with small increments when possible. Unless a
+breaking change is blocking the release, or no fixes/features have been merged, a good
+target release cadence is between every 1 or 2 weeks.
+
+
+## Creating a release
+
+This release process itself should be as automated as possible, and has only a few steps:
+
+1. **Trigger a new release with `make release`**. At this point you'll see a preview
+ changelog in the terminal. If you're happy with the changelog, press `y` to continue, otherwise
+ you can abort and adjust the labels on the PRs and issues to be included in the release and
+ re-run the release trigger command.
+
+1. A release admin must approve the release on the GitHub Actions [release pipeline](https://github.com/anchore/grype/actions/workflows/release.yaml) run page.
+ Once approved, the release pipeline will generate all assets and publish a GitHub Release.
+
+
+## Retracting a release
+
+If a release is found to be problematic, it can be retracted with the following steps:
+
+- Deleting the GitHub Release
+- Untag the docker images in the `ghcr.io` and `docker.io` registries
+- Revert the brew formula in [`anchore/homebrew-grype`](https://github.com/anchore/homebrew-grype) to point to the previous release
+- Add a new `retract` entry in the go.mod for the versioned release
+
+**Note**: do not delete release tags from the git repository since there may already be references to the release
+in the go proxy, which will cause confusion when trying to reuse the tag later (the H1 hash will not match and there
+will be a warning when users try to pull the new release).
+
diff --git a/Taskfile.yaml b/Taskfile.yaml
new file mode 100644
index 00000000000..f7cda008a4c
--- /dev/null
+++ b/Taskfile.yaml
@@ -0,0 +1,439 @@
+version: "3"
+vars:
+ OWNER: anchore
+ PROJECT: grype
+
+ # static file dirs
+ TOOL_DIR: .tool
+ TMP_DIR: .tmp
+
+ # used for changelog generation
+ CHANGELOG: CHANGELOG.md
+ NEXT_VERSION: VERSION
+
+ # used for snapshot builds
+ OS:
+ sh: uname -s | tr '[:upper:]' '[:lower:]'
+ ARCH:
+ sh: |
+ [ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || echo $(uname -m)
+ PROJECT_ROOT:
+ sh: echo $PWD
+ # note: the snapshot dir must be a relative path starting with ./
+ SNAPSHOT_DIR: ./snapshot
+ SNAPSHOT_BIN: "{{ .PROJECT_ROOT }}/{{ .SNAPSHOT_DIR }}/{{ .OS }}-build_{{ .OS }}_{{ .ARCH }}/{{ .PROJECT }}"
+ SNAPSHOT_CMD: "{{ .TOOL_DIR }}/goreleaser release --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --skip=publish --skip=sign"
+ BUILD_CMD: "{{ .TOOL_DIR }}/goreleaser build --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --single-target"
+ RELEASE_CMD: "{{ .TOOL_DIR }}/goreleaser release --clean --release-notes {{ .CHANGELOG }}"
+ VERSION:
+ sh: git describe --dirty --always --tags
+
+ # used for install and acceptance testing
+ COMPARE_DIR: ./test/compare
+ COMPARE_TEST_IMAGE: centos:8.2.2004
+
+env:
+ SYFT_CHECK_FOR_APP_UPDATE: false
+ GRYPE_CHECK_FOR_APP_UPDATE: false
+
+tasks:
+
+ ## High-level tasks #################################
+
+ default:
+ desc: Run all validation tasks
+ aliases:
+ - pr-validations
+ - validations
+ cmds:
+ - task: static-analysis
+ - task: test
+ - task: install-test
+
+ static-analysis:
+ desc: Run all static analysis tasks
+ cmds:
+ - task: check-go-mod-tidy
+ - task: check-licenses
+ - task: lint
+ - task: check-json-schema-drift
+# TODO: while developing v6, we need to disable this check (since v5 and v6 are imported in the same codebase)
+# - task: validate-grype-db-schema
+
+ test:
+ desc: Run all levels of test
+ cmds:
+ - task: unit
+ - task: integration
+ - task: cli
+
+ ## Bootstrap tasks #################################
+
+ binny:
+ internal: true
+ # desc: Get the binny tool
+ generates:
+ - "{{ .TOOL_DIR }}/binny"
+ status:
+ - "test -f {{ .TOOL_DIR }}/binny"
+ cmd: "curl -sSfL https://raw.githubusercontent.com/anchore/binny/main/install.sh | sh -s -- -b .tool"
+ silent: true
+
+ tools:
+ desc: Install all tools needed for CI and local development
+ deps: [binny]
+ aliases:
+ - bootstrap
+ generates:
+ - ".binny.yaml"
+ - "{{ .TOOL_DIR }}/*"
+ status:
+ - "{{ .TOOL_DIR }}/binny check -v"
+ cmd: "{{ .TOOL_DIR }}/binny install -v"
+ silent: true
+
+ update-tools:
+ desc: Update pinned versions of all tools to their latest available versions
+ deps: [binny]
+ generates:
+ - ".binny.yaml"
+ - "{{ .TOOL_DIR }}/*"
+ cmd: "{{ .TOOL_DIR }}/binny update -v"
+ silent: true
+
+ update-quality-gate-db:
+ desc: Update pinned version of quality gate database
+ cmds:
+ - cmd: "go run cmd/grype/main.go db list -o json | jq -r '.[0].path' > test/quality/test-db"
+ silent: true
+
+ list-tools:
+ desc: List all tools needed for CI and local development
+ deps: [binny]
+ cmd: "{{ .TOOL_DIR }}/binny list"
+ silent: true
+
+ list-tool-updates:
+ desc: List all tools that are not up to date relative to the binny config
+ deps: [binny]
+ cmd: "{{ .TOOL_DIR }}/binny list --updates"
+ silent: true
+
+ tmpdir:
+ silent: true
+ generates:
+ - "{{ .TMP_DIR }}"
+ cmd: "mkdir -p {{ .TMP_DIR }}"
+
+ ## Static analysis tasks #################################
+
+ format:
+ desc: Auto-format all source code
+ deps: [tools]
+ cmds:
+ - gofmt -w -s .
+ - "{{ .TOOL_DIR }}/gosimports -local github.com/anchore -w ."
+ - go mod tidy
+
+ lint-fix:
+ desc: Auto-format all source code + run golangci lint fixers
+ deps: [tools]
+ cmds:
+ - task: format
+ - "{{ .TOOL_DIR }}/golangci-lint run --tests=false --fix"
+
+ lint:
+ desc: Run gofmt + golangci lint checks
+ vars:
+ BAD_FMT_FILES:
+ sh: gofmt -l -s .
+ BAD_FILE_NAMES:
+ sh: "find . | grep -e ':' | grep -v -e 'test/quality/.yardstick' -e 'test/quality/vulnerability-match-labels' || true"
+ deps: [tools]
+ cmds:
+ # ensure there are no go fmt differences
+ - cmd: 'test -z "{{ .BAD_FMT_FILES }}" || (echo "files with gofmt issues: [{{ .BAD_FMT_FILES }}]"; exit 1)'
+ silent: true
+ # ensure there are no files with ":" in it (a known back case in the go ecosystem)
+ - cmd: 'test -z "{{ .BAD_FILE_NAMES }}" || (echo "files with bad names: [{{ .BAD_FILE_NAMES }}]"; exit 1)'
+ silent: true
+ # run linting
+ - "{{ .TOOL_DIR }}/golangci-lint run --tests=false"
+
+ check-licenses:
+ # desc: Ensure transitive dependencies are compliant with the current license policy
+ deps: [tools]
+ cmd: "{{ .TOOL_DIR }}/bouncer check ./..."
+
+ check-go-mod-tidy:
+ # desc: Ensure go.mod and go.sum are up to date
+ cmds:
+ - cmd: .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
+ silent: true
+
+ check-json-schema-drift:
+ desc: Ensure there is no drift between the JSON schema and the code
+ cmds:
+ - .github/scripts/json-schema-drift-check.sh
+
+ validate-grype-db-schema:
+ desc: Ensure the codebase is only referencing a single grype-db schema version (multiple is not allowed)
+ cmds:
+ - python test/validate-grype-db-schema.py
+
+
+ ## Testing tasks #################################
+
+ unit:
+ desc: Run unit tests
+ deps:
+ - tmpdir
+ vars:
+ TEST_PKGS:
+ sh: "go list ./... | grep -v {{ .OWNER }}/{{ .PROJECT }}/test | grep -v {{ .OWNER }}/{{ .PROJECT }}/internal/test | tr '\n' ' '"
+
+ # unit test coverage threshold (in % coverage)
+ COVERAGE_THRESHOLD: 47
+ cmds:
+ - "go test -coverprofile {{ .TMP_DIR }}/unit-coverage-details.txt {{ .TEST_PKGS }}"
+ - cmd: ".github/scripts/coverage.py {{ .COVERAGE_THRESHOLD }} {{ .TMP_DIR }}/unit-coverage-details.txt"
+ silent: true
+
+ integration:
+ desc: Run integration tests
+ cmds:
+ - "go test -v ./test/integration"
+ # exercise most of the CLI with the data race detector
+ - "go run -race cmd/{{ .PROJECT }}/main.go alpine:latest"
+
+ cli:
+ desc: Run CLI tests
+ # note: we don't want to regenerate the snapshot unless we have to. In CI it's probable
+ # that the cache being restored with the correct binary will be rebuilt since the timestamps
+ # and local checksums will not line up.
+ deps: [tools, snapshot]
+ sources:
+ - "{{ .SNAPSHOT_BIN }}"
+ - ./test/cli/**
+ - ./**/*.go
+ cmds:
+ - cmd: "echo 'testing binary: {{ .SNAPSHOT_BIN }}'"
+ silent: true
+
+ - cmd: "test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\nno snapshot found' && false)"
+ silent: true
+
+ - "go test -count=1 -timeout=15m -v ./test/cli"
+
+ quality:
+ desc: Run quality tests
+ cmds:
+ - "cd test/quality && make"
+
+
+ ## Test-fixture-related targets #################################
+
+ fingerprints:
+ desc: Generate test fixture fingerprints
+ generates:
+ - test/integration/test-fixtures/cache.fingerprint
+ - test/install/cache.fingerprint
+ - test/cli/test-fixtures/cache.fingerprint
+ cmds:
+ # for IMAGE integration test fixtures
+ - "cd test/integration/test-fixtures && make cache.fingerprint"
+ # for INSTALL integration test fixtures
+ - "cd test/install && make cache.fingerprint"
+ # for CLI test fixtures
+ - "cd test/cli/test-fixtures && make cache.fingerprint"
+
+ show-test-image-cache:
+ silent: true
+ cmds:
+ - "echo '\nDocker daemon cache:'"
+ - "docker images --format '{{`{{.ID}}`}} {{`{{.Repository}}`}}:{{`{{.Tag}}`}}' | grep stereoscope-fixture- | sort"
+ - "echo '\nTar cache:'"
+ - 'find . -type f -wholename "**/test-fixtures/snapshot/*" | sort'
+
+
+ ## install.sh testing targets #################################
+
+ install-test:
+ cmds:
+ - "cd test/install && make"
+
+ install-test-cache-save:
+ cmds:
+ - "cd test/install && make save"
+
+ install-test-cache-load:
+ cmds:
+ - "cd test/install && make load"
+
+ install-test-ci-mac:
+ cmds:
+ - "cd test/install && make ci-test-mac"
+
+ generate-compare-file:
+ cmd: "go run ./cmd/{{ .PROJECT }} {{ .COMPARE_TEST_IMAGE }} -o json > {{ .COMPARE_DIR }}/test-fixtures/acceptance-{{ .COMPARE_TEST_IMAGE }}.json"
+
+ compare-mac:
+ deps: [tmpdir]
+ cmd: |
+ {{ .COMPARE_DIR }}/mac.sh \
+ {{ .SNAPSHOT_DIR }} \
+ {{ .COMPARE_DIR }} \
+ {{ .COMPARE_TEST_IMAGE }} \
+ {{ .TMP_DIR }}
+
+ compare-linux:
+ cmds:
+ - task: compare-test-deb-package-install
+ - task: compare-test-rpm-package-install
+
+ compare-test-deb-package-install:
+ deps: [tmpdir]
+ cmd: |
+ {{ .COMPARE_DIR }}/deb.sh \
+ {{ .SNAPSHOT_DIR }} \
+ {{ .COMPARE_DIR }} \
+ {{ .COMPARE_TEST_IMAGE }} \
+ {{ .TMP_DIR }}
+
+ compare-test-rpm-package-install:
+ deps: [tmpdir]
+ cmd: |
+ {{ .COMPARE_DIR }}/rpm.sh \
+ {{ .SNAPSHOT_DIR }} \
+ {{ .COMPARE_DIR }} \
+ {{ .COMPARE_TEST_IMAGE }} \
+ {{ .TMP_DIR }}
+
+
+ ## Code and data generation targets #################################
+
+ generate:
+ desc: Run code and data generation tasks
+ cmds:
+ - task: generate-json-schema
+
+ generate-json-schema:
+ desc: Generate a new JSON schema
+ cmds:
+ # re-generate package metadata
+ - "cd grype/internal && go generate"
+ # generate the JSON schema for the CLI output
+ - "cd cmd/grype/cli/commands/internal/jsonschema && go run ."
+
+
+ ## Build-related targets #################################
+
+ build:
+ desc: Build the project
+ deps: [tools, tmpdir]
+ generates:
+ - "{{ .PROJECT }}"
+ cmds:
+ - silent: true
+ cmd: |
+ echo "dist: {{ .SNAPSHOT_DIR }}" > {{ .TMP_DIR }}/goreleaser.yaml
+ cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml
+
+ - "{{ .BUILD_CMD }}"
+
+ snapshot:
+ desc: Create a snapshot release
+ aliases:
+ - build
+ deps: [tools, tmpdir]
+ sources:
+ - cmd/**/*.go
+ - "{{ .PROJECT }}/**/*.go"
+ - internal/**/*.go
+ method: checksum
+ generates:
+ - "{{ .SNAPSHOT_BIN }}"
+ cmds:
+ - silent: true
+ cmd: |
+ echo "dist: {{ .SNAPSHOT_DIR }}" > {{ .TMP_DIR }}/goreleaser.yaml
+ cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml
+
+ - "{{ .SNAPSHOT_CMD }}"
+
+ changelog:
+ desc: Generate a changelog
+ deps: [tools]
+ generates:
+ - "{{ .CHANGELOG }}"
+ - "{{ .NEXT_VERSION }}"
+ cmds:
+ - "{{ .TOOL_DIR }}/chronicle -vv -n --version-file {{ .NEXT_VERSION }} > {{ .CHANGELOG }}"
+ - "{{ .TOOL_DIR }}/glow -w 0 {{ .CHANGELOG }}"
+
+
+ ## Release targets #################################
+
+ release:
+ desc: Create a release
+ interactive: true
+ deps: [tools]
+ cmds:
+ - cmd: .github/scripts/trigger-release.sh
+ silent: true
+
+
+ ## CI-only targets #################################
+
+ ci-check:
+ # desc: "[CI only] Are you in CI?"
+ cmds:
+ - cmd: .github/scripts/ci-check.sh
+ silent: true
+
+ ci-release:
+ # desc: "[CI only] Create a release"
+ deps: [tools]
+ cmds:
+ - task: ci-check
+ - "{{ .TOOL_DIR }}/chronicle -vvv > CHANGELOG.md"
+ - cmd: "cat CHANGELOG.md"
+ silent: true
+ - "{{ .RELEASE_CMD }}"
+
+ ci-release-version-file:
+ # desc: "[CI only] Update the version file"
+ deps: [tools]
+ cmds:
+ - task: ci-check
+ - ".github/scripts/update-version-file.sh {{ .RELEASE_VERSION }}"
+ requires:
+ vars:
+ - RELEASE_VERSION
+
+ ci-validate-test-config:
+ # desc: "[CI only] Ensure the update URL is not overridden (not pointing to staging)"
+ silent: true
+ cmd: |
+ bash -c '\
+ grep -q "update-url" test/grype-test-config.yaml; \
+ if [ $? -eq 0 ]; then \
+ echo "Found \"update-url\" in CLI testing config. Cannot release if previous CLI testing did not use production (default) values"; \
+ else
+ echo "Test configuration valid"
+ fi'
+
+
+ ## Cleanup targets #################################
+
+ clean-snapshot:
+ desc: Remove any snapshot builds
+ cmds:
+ - "rm -rf {{ .SNAPSHOT_DIR }}"
+ - "rm -rf {{ .TMP_DIR }}/goreleaser.yaml"
+
+ clean-cache:
+ desc: Remove all docker cache and local image tar cache
+ cmds:
+ - 'find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" -delete'
+ - "docker images --format '{{`{{.ID}}`}} {{`{{.Repository}}`}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs -r docker rmi --force"
diff --git a/cmd/grype/cli/cli.go b/cmd/grype/cli/cli.go
new file mode 100644
index 00000000000..49d887db338
--- /dev/null
+++ b/cmd/grype/cli/cli.go
@@ -0,0 +1,156 @@
+package cli
+
+import (
+ "errors"
+ "os"
+ "runtime/debug"
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+ "github.com/muesli/termenv"
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/commands"
+ 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"
+ "github.com/anchore/stereoscope"
+ syftHandler "github.com/anchore/syft/cmd/syft/cli/ui"
+ "github.com/anchore/syft/syft"
+)
+
+func Application(id clio.Identification) clio.Application {
+ app, _ := create(id)
+ return app
+}
+
+func Command(id clio.Identification) *cobra.Command {
+ _, cmd := create(id)
+ return cmd
+}
+
+func SetupConfig(id clio.Identification) *clio.SetupConfig {
+ return clio.NewSetupConfig(id).
+ WithGlobalConfigFlag(). // add persistent -c for reading an application config from
+ WithGlobalLoggingFlags(). // add persistent -v and -q flags tied to the logging config
+ WithConfigInRootHelp(). // --help on the root command renders the full application config in the help text
+ 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
+ }
+
+ return clio.NewUICollection(
+ ui.New(cfg.Log.Quiet,
+ grypeHandler.New(grypeHandler.DefaultHandlerConfig()),
+ syftHandler.New(syftHandler.DefaultHandlerConfig()),
+ ),
+ noUI,
+ ), nil
+ },
+ ).
+ WithInitializers(
+ func(state *clio.State) error {
+ // clio is setting up and providing the bus, redact store, and logger to the application. Once loaded,
+ // we can hoist them into the internal packages for global use.
+ stereoscope.SetBus(state.Bus)
+ syft.SetBus(state.Bus)
+ bus.Set(state.Bus)
+
+ redact.Set(state.RedactStore)
+
+ log.Set(state.Logger)
+ syft.SetLogger(state.Logger)
+ stereoscope.SetLogger(state.Logger)
+
+ return nil
+ },
+ ).
+ 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
+ })
+}
+
+func create(id clio.Identification) (clio.Application, *cobra.Command) {
+ clioCfg := SetupConfig(id)
+
+ app := clio.New(*clioCfg)
+
+ rootCmd := commands.Root(app)
+
+ // add sub-commands
+ rootCmd.AddCommand(
+ commands.DB(app),
+ commands.Completion(app),
+ commands.Explain(app),
+ clio.VersionCommand(id, syftVersion, dbVersion),
+ clio.ConfigCommand(app, nil),
+ )
+
+ return app, rootCmd
+}
+
+func syftVersion() (string, any) {
+ buildInfo, ok := debug.ReadBuildInfo()
+ if !ok {
+ log.Debug("unable to find the buildinfo section of the binary (syft version is unknown)")
+ return "", ""
+ }
+
+ for _, d := range buildInfo.Deps {
+ if d.Path == "github.com/anchore/syft" {
+ return "Syft Version", d.Version
+ }
+ }
+
+ log.Debug("unable to find 'github.com/anchore/syft' from the buildinfo section of the binary")
+ return "", ""
+}
+
+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/cli_test.go b/cmd/grype/cli/cli_test.go
new file mode 100644
index 00000000000..8e39d4bb908
--- /dev/null
+++ b/cmd/grype/cli/cli_test.go
@@ -0,0 +1,19 @@
+package cli
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/clio"
+)
+
+func Test_Command(t *testing.T) {
+ root := Command(clio.Identification{
+ Name: "test-name",
+ Version: "test-version",
+ })
+
+ require.Equal(t, root.Name(), "test-name")
+ require.NotEmpty(t, root.Commands())
+}
diff --git a/cmd/grype/cli/legacy/completion.go b/cmd/grype/cli/commands/completion.go
similarity index 69%
rename from cmd/grype/cli/legacy/completion.go
rename to cmd/grype/cli/commands/completion.go
index ae7dd3e0906..6ad050b8069 100644
--- a/cmd/grype/cli/legacy/completion.go
+++ b/cmd/grype/cli/commands/completion.go
@@ -1,21 +1,24 @@
-package legacy
+package commands
import (
"context"
"os"
"strings"
- "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
+ "github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
)
-// completionCmd represents the completion command
-var completionCmd = &cobra.Command{
- Use: "completion [bash|zsh|fish]",
- Short: "Generate a shell completion for Grype (listing local docker images)",
- Long: `To load completions (docker image list):
+// Completion returns a command to provide completion to various terminal shells
+func Completion(app clio.Application) *cobra.Command {
+ return &cobra.Command{
+ Use: "completion [bash|zsh|fish]",
+ Short: "Generate a shell completion for Grype (listing local docker images)",
+ Long: `To load completions (docker image list):
Bash:
@@ -46,40 +49,23 @@ $ grype completion fish | source
# To load completions for each session, execute once:
$ grype completion fish > ~/.config/fish/completions/grype.fish
`,
- DisableFlagsInUseLine: true,
- ValidArgs: []string{"bash", "fish", "zsh"},
- Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
- RunE: func(cmd *cobra.Command, args []string) error {
- var err error
- switch args[0] {
- case "zsh":
- err = cmd.Root().GenZshCompletion(os.Stdout)
- case "bash":
- err = cmd.Root().GenBashCompletion(os.Stdout)
- case "fish":
- err = cmd.Root().GenFishCompletion(os.Stdout, true)
- }
- return err
- },
-}
-
-func init() {
- rootCmd.AddCommand(completionCmd)
-}
-
-func dockerImageValidArgsFunction(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- // Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided
- dockerImageRepoTags, err := listLocalDockerImages(toComplete)
- if err != nil {
- // Indicates that an error occurred and completions should be ignored
- return []string{"completion failed"}, cobra.ShellCompDirectiveError
- }
- if len(dockerImageRepoTags) == 0 {
- return []string{"no docker images found"}, cobra.ShellCompDirectiveError
+ DisableFlagsInUseLine: true,
+ ValidArgs: []string{"bash", "fish", "zsh"},
+ PreRunE: disableUI(app),
+ Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ var err error
+ switch args[0] {
+ case "zsh":
+ err = cmd.Root().GenZshCompletion(os.Stdout)
+ case "bash":
+ err = cmd.Root().GenBashCompletion(os.Stdout)
+ case "fish":
+ err = cmd.Root().GenFishCompletion(os.Stdout, true)
+ }
+ return err
+ },
}
- // ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have
- // been provided (without implying other possible directives)
- return dockerImageRepoTags, cobra.ShellCompDirectiveDefault
}
func listLocalDockerImages(prefix string) ([]string, error) {
@@ -93,7 +79,7 @@ func listLocalDockerImages(prefix string) ([]string, error) {
// Only want to return tagged images
imageListArgs := filters.NewArgs()
imageListArgs.Add("dangling", "false")
- images, err := cli.ImageList(ctx, types.ImageListOptions{All: false, Filters: imageListArgs})
+ images, err := cli.ImageList(ctx, image.ListOptions{All: false, Filters: imageListArgs})
if err != nil {
return repoTags, err
}
@@ -108,3 +94,18 @@ func listLocalDockerImages(prefix string) ([]string, error) {
}
return repoTags, nil
}
+
+func dockerImageValidArgsFunction(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ // Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided
+ dockerImageRepoTags, err := listLocalDockerImages(toComplete)
+ if err != nil {
+ // Indicates that an error occurred and completions should be ignored
+ return []string{"completion failed"}, cobra.ShellCompDirectiveError
+ }
+ if len(dockerImageRepoTags) == 0 {
+ return []string{"no docker images found"}, cobra.ShellCompDirectiveError
+ }
+ // ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have
+ // been provided (without implying other possible directives)
+ return dockerImageRepoTags, cobra.ShellCompDirectiveDefault
+}
diff --git a/cmd/grype/cli/commands/db.go b/cmd/grype/cli/commands/db.go
new file mode 100644
index 00000000000..2de3c4bd176
--- /dev/null
+++ b/cmd/grype/cli/commands/db.go
@@ -0,0 +1,33 @@
+package commands
+
+import (
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+)
+
+const (
+ jsonOutputFormat = "json"
+ tableOutputFormat = "table"
+ textOutputFormat = "text"
+)
+
+func DB(app clio.Application) *cobra.Command {
+ db := &cobra.Command{
+ Use: "db",
+ Short: "vulnerability database operations",
+ }
+
+ db.AddCommand(
+ DBCheck(app),
+ DBDelete(app),
+ DBImport(app),
+ DBList(app),
+ DBStatus(app),
+ DBUpdate(app),
+ DBSearch(app),
+ DBProviders(app),
+ )
+
+ return db
+}
diff --git a/cmd/grype/cli/commands/db_check.go b/cmd/grype/cli/commands/db_check.go
new file mode 100644
index 00000000000..20d035636c5
--- /dev/null
+++ b/cmd/grype/cli/commands/db_check.go
@@ -0,0 +1,129 @@
+package commands
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "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"
+)
+
+type dbCheckOptions struct {
+ Output string `yaml:"output" json:"output" mapstructure:"output"`
+ options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+}
+
+var _ clio.FlagAdder = (*dbCheckOptions)(nil)
+
+func (d *dbCheckOptions) AddFlags(flags clio.FlagSet) {
+ flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, json])")
+}
+
+func DBCheck(app clio.Application) *cobra.Command {
+ opts := &dbCheckOptions{
+ Output: textOutputFormat,
+ DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
+ }
+
+ cmd := &cobra.Command{
+ Use: "check",
+ Short: "Check to see if there is a database update available",
+ PreRunE: func(cmd *cobra.Command, args []string) error {
+ // DB commands should not opt into the low-pass check filter
+ opts.DB.MaxUpdateCheckFrequency = 0
+ return disableUI(app)(cmd, args)
+ },
+ Args: cobra.ExactArgs(0),
+ RunE: func(_ *cobra.Command, _ []string) error {
+ return runDBCheck(*opts)
+ },
+ }
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ Hidden *dbCheckOptions `json:"-" yaml:"-" mapstructure:"-"`
+ *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
+}
+
+func runDBCheck(opts dbCheckOptions) error {
+ client, err := distribution.NewClient(opts.ToClientConfig())
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+
+ cfg := opts.ToCuratorConfig()
+
+ current, err := db.ReadDescription(cfg.DBFilePath())
+ if err != nil {
+ log.WithFields("error", err).Debug("unable to read current database metadata")
+ current = nil
+ }
+
+ archive, err := client.IsUpdateAvailable(current)
+ if err != nil {
+ return fmt.Errorf("unable to check for vulnerability database update: %w", err)
+ }
+
+ updateAvailable := archive != nil
+
+ if err := presentNewDBCheck(opts.Output, os.Stdout, updateAvailable, current, archive); err != nil {
+ return err
+ }
+
+ if updateAvailable {
+ return grypeerr.ErrDBUpgradeAvailable
+ }
+ return nil
+}
+
+type dbCheckJSON struct {
+ CurrentDB *db.Description `json:"currentDB"`
+ CandidateDB *distribution.Archive `json:"candidateDB"`
+ UpdateAvailable bool `json:"updateAvailable"`
+}
+
+func presentNewDBCheck(format string, writer io.Writer, updateAvailable bool, current *db.Description, candidate *distribution.Archive) error {
+ switch format {
+ case textOutputFormat:
+ if current != nil {
+ fmt.Fprintf(writer, "Installed DB version %s was built on %s\n", current.SchemaVersion, current.Built.String())
+ } else {
+ fmt.Fprintln(writer, "No installed DB version found")
+ }
+
+ if !updateAvailable {
+ fmt.Fprintln(writer, "No update available")
+ return nil
+ }
+
+ fmt.Fprintf(writer, "Updated DB version %s was built on %s\n", candidate.SchemaVersion, candidate.Built.String())
+ fmt.Fprintln(writer, "You can run 'grype db update' to update to the latest db")
+ case jsonOutputFormat:
+ data := dbCheckJSON{
+ CurrentDB: current,
+ CandidateDB: candidate,
+ UpdateAvailable: updateAvailable,
+ }
+
+ enc := json.NewEncoder(writer)
+ enc.SetEscapeHTML(false)
+ enc.SetIndent("", " ")
+ if err := enc.Encode(&data); err != nil {
+ return fmt.Errorf("failed to db listing information: %+v", err)
+ }
+ default:
+ return fmt.Errorf("unsupported output format: %s", format)
+ }
+ return nil
+}
diff --git a/cmd/grype/cli/commands/db_check_test.go b/cmd/grype/cli/commands/db_check_test.go
new file mode 100644
index 00000000000..82c5026c3af
--- /dev/null
+++ b/cmd/grype/cli/commands/db_check_test.go
@@ -0,0 +1,132 @@
+package commands
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ db "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+func TestPresentNewDBCheck(t *testing.T) {
+ currentDB := &db.Description{
+ SchemaVersion: schemaver.New(6, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 11, 25, 12, 0, 0, 0, time.UTC)},
+ }
+
+ candidateDB := &distribution.Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(6, 0, 1),
+ Built: db.Time{Time: time.Date(2023, 11, 26, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "vulnerability-db_6.0.1_2023-11-26T12:00:00Z_6238463.tar.gz",
+ Checksum: "sha256:1234561234567890345674561234567890345678",
+ }
+ tests := []struct {
+ name string
+ format string
+ updateAvailable bool
+ current *db.Description
+ candidate *distribution.Archive
+ expectedText string
+ expectErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "text format with update available",
+ format: textOutputFormat,
+ updateAvailable: true,
+ current: currentDB,
+ candidate: candidateDB,
+ expectedText: `
+Installed DB version v6.0.0 was built on 2023-11-25T12:00:00Z
+Updated DB version v6.0.1 was built on 2023-11-26T12:00:00Z
+You can run 'grype db update' to update to the latest db
+`,
+ },
+ {
+ name: "text format without update available",
+ format: textOutputFormat,
+ updateAvailable: false,
+ current: currentDB,
+ candidate: nil,
+ expectedText: `
+Installed DB version v6.0.0 was built on 2023-11-25T12:00:00Z
+No update available
+`,
+ },
+ {
+ name: "json format with update available",
+ format: jsonOutputFormat,
+ updateAvailable: true,
+ current: currentDB,
+ candidate: candidateDB,
+ expectedText: `
+{
+ "currentDB": {
+ "schemaVersion": "v6.0.0",
+ "built": "2023-11-25T12:00:00Z"
+ },
+ "candidateDB": {
+ "schemaVersion": "v6.0.1",
+ "built": "2023-11-26T12:00:00Z",
+ "path": "vulnerability-db_6.0.1_2023-11-26T12:00:00Z_6238463.tar.gz",
+ "checksum": "sha256:1234561234567890345674561234567890345678"
+ },
+ "updateAvailable": true
+}
+`,
+ },
+ {
+ name: "json format without update available",
+ format: jsonOutputFormat,
+ updateAvailable: false,
+ current: currentDB,
+ candidate: nil,
+ expectedText: `
+{
+ "currentDB": {
+ "schemaVersion": "v6.0.0",
+ "built": "2023-11-25T12:00:00Z"
+ },
+ "candidateDB": null,
+ "updateAvailable": false
+}
+`,
+ },
+ {
+ name: "unsupported format",
+ format: "xml",
+ expectErr: requireErrorContains("unsupported output format: xml"),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectErr == nil {
+ tt.expectErr = require.NoError
+ }
+ buf := &bytes.Buffer{}
+ err := presentNewDBCheck(tt.format, buf, tt.updateAvailable, tt.current, tt.candidate)
+
+ tt.expectErr(t, err)
+ if err != nil {
+ return
+ }
+
+ assert.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(buf.String()))
+ })
+ }
+}
+
+func requireErrorContains(expected string) require.ErrorAssertionFunc {
+ return func(t require.TestingT, err error, msgAndArgs ...interface{}) {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), expected)
+ }
+}
diff --git a/cmd/grype/cli/commands/db_delete.go b/cmd/grype/cli/commands/db_delete.go
new file mode 100644
index 00000000000..fea42a69acc
--- /dev/null
+++ b/cmd/grype/cli/commands/db_delete.go
@@ -0,0 +1,50 @@
+package commands
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/options"
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ "github.com/anchore/grype/grype/db/v6/installation"
+)
+
+func DBDelete(app clio.Application) *cobra.Command {
+ opts := options.DefaultDatabaseCommand(app.ID())
+
+ cmd := &cobra.Command{
+ Use: "delete",
+ Short: "Delete the vulnerability database",
+ Args: cobra.ExactArgs(0),
+ PreRunE: disableUI(app),
+ RunE: func(_ *cobra.Command, _ []string) error {
+ return runDBDelete(*opts)
+ },
+ }
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{opts})
+}
+
+func runDBDelete(opts options.DatabaseCommand) error {
+ client, err := distribution.NewClient(opts.ToClientConfig())
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+ c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
+ if err != nil {
+ return fmt.Errorf("unable to create curator: %w", err)
+ }
+
+ if err := c.Delete(); err != nil {
+ return fmt.Errorf("unable to delete vulnerability database: %+v", err)
+ }
+
+ return stderrPrintLnf("Vulnerability database deleted")
+}
diff --git a/cmd/grype/cli/commands/db_import.go b/cmd/grype/cli/commands/db_import.go
new file mode 100644
index 00000000000..587ce08e281
--- /dev/null
+++ b/cmd/grype/cli/commands/db_import.go
@@ -0,0 +1,55 @@
+package commands
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/options"
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ "github.com/anchore/grype/grype/db/v6/installation"
+ "github.com/anchore/grype/internal/log"
+)
+
+func DBImport(app clio.Application) *cobra.Command {
+ opts := options.DefaultDatabaseCommand(app.ID())
+
+ cmd := &cobra.Command{
+ 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])
+ },
+ }
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{opts})
+}
+
+func runDBImport(opts options.DatabaseCommand, reference string) error {
+ // TODO: tui update? better logging?
+ client, err := distribution.NewClient(opts.ToClientConfig())
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+ c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
+ if err != nil {
+ return fmt.Errorf("unable to create curator: %w", err)
+ }
+
+ 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", 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
new file mode 100644
index 00000000000..3be6a4a9d7b
--- /dev/null
+++ b/cmd/grype/cli/commands/db_list.go
@@ -0,0 +1,112 @@
+package commands
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/url"
+ "os"
+
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/options"
+ "github.com/anchore/grype/grype/db/v6/distribution"
+)
+
+type dbListOptions struct {
+ Output string `yaml:"output" json:"output" mapstructure:"output"`
+ options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+}
+
+var _ clio.FlagAdder = (*dbListOptions)(nil)
+
+func (d *dbListOptions) AddFlags(flags clio.FlagSet) {
+ flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, raw, json])")
+}
+
+func DBList(app clio.Application) *cobra.Command {
+ opts := &dbListOptions{
+ Output: textOutputFormat,
+ DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
+ }
+
+ cmd := &cobra.Command{
+ Use: "list",
+ Short: "List all DBs available according to the listing URL",
+ PreRunE: disableUI(app),
+ Args: cobra.ExactArgs(0),
+ RunE: func(_ *cobra.Command, _ []string) error {
+ return runDBList(*opts)
+ },
+ }
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ Hidden *dbListOptions `json:"-" yaml:"-" mapstructure:"-"`
+ *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
+}
+
+func runDBList(opts dbListOptions) error {
+ c, err := distribution.NewClient(opts.ToClientConfig())
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+
+ latest, err := c.Latest()
+ if err != nil {
+ return fmt.Errorf("unable to get database listing: %w", err)
+ }
+
+ 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, archiveURL, listingURL string, writer io.Writer, latest *distribution.LatestDocument) error {
+ if latest == nil {
+ return fmt.Errorf("no database listing found")
+ }
+
+ // remove query params
+ archiveURLObj, err := url.Parse(archiveURL)
+ if err != nil {
+ return fmt.Errorf("unable to parse db URL %q: %w", archiveURL, err)
+ }
+
+ 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", 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)
+ enc.SetEscapeHTML(false)
+ enc.SetIndent("", " ")
+ // why make an array? We are reserving the right to list additional entries in the future without the
+ // need to change from an object to an array at that point in time. This will be useful if we implement
+ // the history.json functionality for grabbing historical database listings.
+ if err := enc.Encode([]any{latest}); err != nil {
+ return fmt.Errorf("failed to db listing information: %+v", err)
+ }
+ default:
+ return fmt.Errorf("unsupported output format: %s", format)
+ }
+ return nil
+}
diff --git a/cmd/grype/cli/commands/db_list_test.go b/cmd/grype/cli/commands/db_list_test.go
new file mode 100644
index 00000000000..3492f8e4114
--- /dev/null
+++ b/cmd/grype/cli/commands/db_list_test.go
@@ -0,0 +1,167 @@
+package commands
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/clio"
+ "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/internal/schemaver"
+)
+
+func Test_ListingUserAgent(t *testing.T) {
+
+ t.Run("new", func(t *testing.T) {
+ listingFile := "/latest.json"
+
+ got := ""
+
+ // setup mock
+ handler := http.NewServeMux()
+ handler.HandleFunc(listingFile, func(w http.ResponseWriter, r *http.Request) {
+ got = r.Header.Get("User-Agent")
+ w.WriteHeader(http.StatusOK)
+ _ = json.NewEncoder(w).Encode(&distribution.LatestDocument{
+ Status: "active",
+ Archive: distribution.Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(6, 0, 0),
+ Built: db.Time{Time: time.Now()},
+ },
+ Path: "vulnerability-db_v6.0.0.tar.gz",
+ Checksum: "sha256:dummychecksum",
+ },
+ })
+ })
+ mockSrv := httptest.NewServer(handler)
+ defer mockSrv.Close()
+
+ dbOptions := *options.DefaultDatabaseCommand(clio.Identification{
+ Name: "new-app",
+ Version: "v4.0.0",
+ })
+ dbOptions.DB.RequireUpdateCheck = true
+ dbOptions.DB.UpdateURL = mockSrv.URL + listingFile
+
+ err := runDBList(dbListOptions{
+ Output: textOutputFormat,
+ DatabaseCommand: dbOptions,
+ })
+ require.NoError(t, err)
+
+ if got != "new-app v4.0.0" {
+ t.Errorf("expected User-Agent header to match, got: %v", got)
+ }
+ })
+
+}
+
+func TestPresentDBList(t *testing.T) {
+ latestDoc := &distribution.LatestDocument{
+ Status: "active",
+ Archive: distribution.Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(6, 0, 0),
+ Built: db.Time{Time: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC)},
+ },
+ Path: "vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst",
+ Checksum: "sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4",
+ },
+ }
+
+ 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,
+ 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,
+ },
+ {
+ name: "valid JSON format",
+ format: jsonOutputFormat,
+ latest: latestDoc,
+ expectedText: `[
+ {
+ "status": "active",
+ "schemaVersion": "v6.0.0",
+ "built": "2024-11-27T14:43:17Z",
+ "path": "vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst",
+ "checksum": "sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4"
+ }
+]
+`,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "nil latest document",
+ format: textOutputFormat,
+ latest: nil,
+ expectedErr: requireErrorContains("no database listing found"),
+ },
+ {
+ name: "unsupported format",
+ format: "unsupported",
+ latest: latestDoc,
+ expectedErr: requireErrorContains("unsupported output format"),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ writer := &bytes.Buffer{}
+
+ err := presentDBList(tt.format, tt.archiveURL, tt.baseURL, writer, tt.latest)
+ if tt.expectedErr == nil {
+ tt.expectedErr = require.NoError
+ }
+ tt.expectedErr(t, err)
+
+ if err != nil {
+ return
+ }
+
+ require.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(writer.String()))
+ })
+ }
+}
diff --git a/cmd/grype/cli/commands/db_providers.go b/cmd/grype/cli/commands/db_providers.go
new file mode 100644
index 00000000000..9394f9d23d7
--- /dev/null
+++ b/cmd/grype/cli/commands/db_providers.go
@@ -0,0 +1,150 @@
+package commands
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/olekukonko/tablewriter"
+ "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/internal/bus"
+)
+
+type dbProvidersOptions struct {
+ Output string `yaml:"output" json:"output"`
+ options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+}
+
+var _ clio.FlagAdder = (*dbProvidersOptions)(nil)
+
+func (d *dbProvidersOptions) AddFlags(flags clio.FlagSet) {
+ flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[table, json])")
+}
+
+func DBProviders(app clio.Application) *cobra.Command {
+ opts := &dbProvidersOptions{
+ Output: tableOutputFormat,
+ DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
+ }
+
+ cmd := &cobra.Command{
+ Use: "providers",
+ Short: "List vulnerability providers that are in the database",
+ Args: cobra.ExactArgs(0),
+ RunE: func(_ *cobra.Command, _ []string) error {
+ return runDBProviders(opts)
+ },
+ }
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ Hidden *dbProvidersOptions `json:"-" yaml:"-" mapstructure:"-"`
+ *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
+}
+
+func runDBProviders(opts *dbProvidersOptions) error {
+ client, err := distribution.NewClient(opts.ToClientConfig())
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+ c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
+ if err != nil {
+ return fmt.Errorf("unable to create curator: %w", err)
+ }
+
+ reader, err := c.Reader()
+ if err != nil {
+ return fmt.Errorf("unable to get providers: %w", err)
+ }
+
+ providerModels, err := reader.AllProviders()
+ if err != nil {
+ return fmt.Errorf("unable to get providers: %w", err)
+ }
+
+ sb := &strings.Builder{}
+
+ switch opts.Output {
+ case tableOutputFormat, textOutputFormat:
+ displayDBProvidersTable(toProviders(providerModels), sb)
+ case jsonOutputFormat:
+ err = displayDBProvidersJSON(toProviders(providerModels), sb)
+ if err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("unsupported output format: %s", opts.Output)
+ }
+ bus.Report(sb.String())
+
+ return nil
+}
+
+type provider struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Processor string `json:"processor"`
+ DateCaptured *time.Time `json:"dateCaptured"`
+ InputDigest string `json:"inputDigest"`
+}
+
+func toProviders(providers []v6.Provider) []provider {
+ var res []provider
+ for _, p := range providers {
+ res = append(res, provider{
+ Name: p.ID,
+ Version: p.Version,
+ Processor: p.Processor,
+ DateCaptured: p.DateCaptured,
+ InputDigest: p.InputDigest,
+ })
+ }
+ return res
+}
+
+func displayDBProvidersTable(providers []provider, output io.Writer) {
+ rows := [][]string{}
+ for _, provider := range providers {
+ rows = append(rows, []string{provider.Name, provider.Version, provider.Processor, provider.DateCaptured.String(), provider.InputDigest})
+ }
+
+ table := tablewriter.NewWriter(output)
+ table.SetHeader([]string{"Name", "Version", "Processor", "Date Captured", "Input Digest"})
+
+ table.SetHeaderLine(false)
+ table.SetBorder(false)
+ table.SetAutoWrapText(false)
+ table.SetAutoFormatHeaders(true)
+ table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
+ table.SetAlignment(tablewriter.ALIGN_LEFT)
+ table.SetCenterSeparator("")
+ table.SetColumnSeparator("")
+ table.SetRowSeparator("")
+ table.SetTablePadding(" ")
+ table.SetNoWhiteSpace(true)
+
+ table.AppendBulk(rows)
+ table.Render()
+}
+
+func displayDBProvidersJSON(providers []provider, output io.Writer) error {
+ encoder := json.NewEncoder(output)
+ encoder.SetEscapeHTML(false)
+ encoder.SetIndent("", " ")
+ err := encoder.Encode(providers)
+ if err != nil {
+ return fmt.Errorf("cannot display json: %w", err)
+ }
+ return nil
+}
diff --git a/cmd/grype/cli/commands/db_providers_test.go b/cmd/grype/cli/commands/db_providers_test.go
new file mode 100644
index 00000000000..6c4c2cf1ce5
--- /dev/null
+++ b/cmd/grype/cli/commands/db_providers_test.go
@@ -0,0 +1,85 @@
+package commands
+
+import (
+ "bytes"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestDisplayDBProvidersTable(t *testing.T) {
+ providers := []provider{
+ {
+ Name: "provider1",
+ Version: "1.0.0",
+ Processor: "vunnel@3.2",
+ DateCaptured: timeRef(time.Date(2024, 11, 25, 14, 30, 0, 0, time.UTC)),
+ InputDigest: "xxh64:1234567834567",
+ },
+ {
+ Name: "provider2",
+ Version: "2.0.0",
+ Processor: "vunnel@3.2",
+ DateCaptured: timeRef(time.Date(2024, 11, 26, 10, 15, 0, 0, time.UTC)),
+ InputDigest: "xxh64:9876543212345",
+ },
+ }
+
+ expectedOutput := `NAME VERSION PROCESSOR DATE CAPTURED INPUT DIGEST
+provider1 1.0.0 vunnel@3.2 2024-11-25 14:30:00 +0000 UTC xxh64:1234567834567
+provider2 2.0.0 vunnel@3.2 2024-11-26 10:15:00 +0000 UTC xxh64:9876543212345
+`
+
+ var output bytes.Buffer
+ displayDBProvidersTable(providers, &output)
+
+ require.Equal(t, expectedOutput, output.String())
+}
+
+func TestDisplayDBProvidersJSON(t *testing.T) {
+ providers := []provider{
+ {
+ Name: "provider1",
+ Version: "1.0.0",
+ Processor: "vunnel@3.2",
+ DateCaptured: timeRef(time.Date(2024, 11, 25, 14, 30, 0, 0, time.UTC)),
+ InputDigest: "xxh64:1234567834567",
+ },
+ {
+ Name: "provider2",
+ Version: "2.0.0",
+ Processor: "vunnel@3.2",
+ DateCaptured: timeRef(time.Date(2024, 11, 26, 10, 15, 0, 0, time.UTC)),
+ InputDigest: "xxh64:9876543212345",
+ },
+ }
+
+ expectedJSON := `[
+ {
+ "name": "provider1",
+ "version": "1.0.0",
+ "processor": "vunnel@3.2",
+ "dateCaptured": "2024-11-25T14:30:00Z",
+ "inputDigest": "xxh64:1234567834567"
+ },
+ {
+ "name": "provider2",
+ "version": "2.0.0",
+ "processor": "vunnel@3.2",
+ "dateCaptured": "2024-11-26T10:15:00Z",
+ "inputDigest": "xxh64:9876543212345"
+ }
+]
+`
+
+ var output bytes.Buffer
+ err := displayDBProvidersJSON(providers, &output)
+ require.NoError(t, err)
+
+ require.JSONEq(t, expectedJSON, output.String())
+}
+
+func timeRef(t time.Time) *time.Time {
+ return &t
+}
diff --git a/cmd/grype/cli/commands/db_search.go b/cmd/grype/cli/commands/db_search.go
new file mode 100644
index 00000000000..01bbe563d01
--- /dev/null
+++ b/cmd/grype/cli/commands/db_search.go
@@ -0,0 +1,254 @@
+package commands
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch"
+ "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/internal/bus"
+ "github.com/anchore/grype/internal/log"
+)
+
+type dbSearchMatchOptions struct {
+ Format options.DBSearchFormat `yaml:",inline" mapstructure:",squash"`
+ Vulnerability options.DBSearchVulnerabilities `yaml:",inline" mapstructure:",squash"`
+ Package options.DBSearchPackages `yaml:",inline" mapstructure:",squash"`
+ OS options.DBSearchOSs `yaml:",inline" mapstructure:",squash"`
+ Bounds options.DBSearchBounds `yaml:",inline" mapstructure:",squash"`
+
+ options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+}
+
+var alasPattern = regexp.MustCompile(`^alas[\w]*-\d+-\d+$`)
+
+func (o *dbSearchMatchOptions) applyArgs(args []string) error {
+ for _, arg := range args {
+ lowerArg := strings.ToLower(arg)
+ switch {
+ case hasAnyPrefix(lowerArg, "cpe:", "purl:"):
+ // this is explicitly a package...
+ log.WithFields("value", arg).Trace("assuming arg is a package specifier")
+ o.Package.Packages = append(o.Package.Packages, arg)
+ case hasAnyPrefix(lowerArg, "cve-", "ghsa-", "elsa-", "rhsa-") || alasPattern.MatchString(lowerArg):
+ // this is a vulnerability...
+ log.WithFields("value", arg).Trace("assuming arg is a vulnerability ID")
+ o.Vulnerability.VulnerabilityIDs = append(o.Vulnerability.VulnerabilityIDs, arg)
+ default:
+ // assume this is a package name
+ log.WithFields("value", arg).Trace("assuming arg is a package name")
+ o.Package.Packages = append(o.Package.Packages, arg)
+ }
+ }
+
+ if err := o.Vulnerability.PostLoad(); err != nil {
+ return err
+ }
+
+ if err := o.Package.PostLoad(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func hasAnyPrefix(s string, prefixes ...string) bool {
+ for _, prefix := range prefixes {
+ if strings.HasPrefix(s, prefix) {
+ return true
+ }
+ }
+ return false
+}
+
+func DBSearch(app clio.Application) *cobra.Command {
+ opts := &dbSearchMatchOptions{
+ Format: options.DefaultDBSearchFormat(),
+ Vulnerability: options.DBSearchVulnerabilities{
+ UseVulnIDFlag: true,
+ },
+ Bounds: options.DefaultDBSearchBounds(),
+ DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
+ }
+
+ cmd := &cobra.Command{
+ Use: "search",
+ Short: "Search the DB for vulnerabilities or affected packages",
+ Example: `
+ Search for affected packages by vulnerability ID:
+
+ $ grype db search --vuln ELSA-2023-12205
+
+ Search for affected packages by package name:
+
+ $ grype db search --pkg log4j
+
+ Search for affected packages by package name, filtering down to a specific vulnerability:
+
+ $ grype db search --pkg log4j --vuln CVE-2021-44228
+
+ Search for affected packages by PURL (note: version is not considered):
+
+ $ grype db search --pkg 'pkg:rpm/redhat/openssl' # or: '--ecosystem rpm --pkg openssl
+
+ Search for affected packages by CPE (note: version/update is not considered):
+
+ $ grype db search --pkg 'cpe:2.3:a:jetty:jetty_http_server:*:*:*:*:*:*'
+ $ grype db search --pkg 'cpe:/a:jetty:jetty_http_server'`,
+ PreRunE: disableUI(app),
+ RunE: func(cmd *cobra.Command, args []string) (err error) {
+ if len(args) > 0 {
+ // try to stay backwards compatible with v5 search command (which takes args)
+ if err := opts.applyArgs(args); err != nil {
+ return err
+ }
+ }
+ err = runDBSearchMatches(*opts)
+ if err != nil {
+ if errors.Is(err, dbsearch.ErrNoSearchCriteria) {
+ _ = cmd.Usage()
+ }
+ return err
+ }
+ return nil
+ },
+ }
+
+ cmd.AddCommand(
+ DBSearchVulnerabilities(app),
+ )
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ Hidden *dbSearchMatchOptions `json:"-" yaml:"-" mapstructure:"-"`
+ *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
+}
+
+func runDBSearchMatches(opts dbSearchMatchOptions) error {
+ client, err := distribution.NewClient(opts.ToClientConfig())
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+
+ curator, err := installation.NewCurator(opts.ToCuratorConfig(), client)
+ if err != nil {
+ return fmt.Errorf("unable to create curator: %w", err)
+ }
+
+ reader, err := curator.Reader()
+ if err != nil {
+ return fmt.Errorf("unable to get providers: %w", err)
+ }
+
+ if err := validateProvidersFilter(reader, opts.Vulnerability.Providers); err != nil {
+ return err
+ }
+
+ rows, queryErr := dbsearch.FindMatches(reader, dbsearch.AffectedPackagesOptions{
+ Vulnerability: opts.Vulnerability.Specs,
+ Package: opts.Package.PkgSpecs,
+ CPE: opts.Package.CPESpecs,
+ OS: opts.OS.Specs,
+ AllowBroadCPEMatching: opts.Package.AllowBroadCPEMatching,
+ RecordLimit: opts.Bounds.RecordLimit,
+ })
+ if queryErr != nil {
+ if !errors.Is(queryErr, v6.ErrLimitReached) {
+ return queryErr
+ }
+ }
+
+ sb := &strings.Builder{}
+ err = presentDBSearchMatches(opts.Format.Output, rows, sb)
+ rep := sb.String()
+ if rep != "" {
+ bus.Report(rep)
+ }
+ if err != nil {
+ return fmt.Errorf("unable to present search results: %w", err)
+ }
+
+ return queryErr
+}
+
+func presentDBSearchMatches(outputFormat string, structuredRows dbsearch.Matches, output io.Writer) error {
+ switch outputFormat {
+ case tableOutputFormat:
+ if len(structuredRows) == 0 {
+ bus.Notify("No results found")
+ return nil
+ }
+ rows := renderDBSearchPackagesTableRows(structuredRows.Flatten())
+
+ table := newTable(output)
+
+ table.SetHeader([]string{"Vulnerability", "Package", "Ecosystem", "Namespace", "Version Constraint"})
+ table.AppendBulk(rows)
+ table.Render()
+ case jsonOutputFormat:
+ if structuredRows == nil {
+ // always allocate the top level collection
+ structuredRows = dbsearch.Matches{}
+ }
+ enc := json.NewEncoder(output)
+ enc.SetEscapeHTML(false)
+ enc.SetIndent("", " ")
+ if err := enc.Encode(structuredRows); err != nil {
+ return fmt.Errorf("failed to encode diff information: %+v", err)
+ }
+ default:
+ return fmt.Errorf("unsupported output format: %s", outputFormat)
+ }
+ return nil
+}
+
+func renderDBSearchPackagesTableRows(structuredRows []dbsearch.AffectedPackage) [][]string {
+ var rows [][]string
+ for _, rr := range structuredRows {
+ var pkgOrCPE, ecosystem string
+ if rr.Package != nil {
+ pkgOrCPE = rr.Package.Name
+ ecosystem = rr.Package.Ecosystem
+ } else if rr.CPE != nil {
+ pkgOrCPE = rr.CPE.String()
+ ecosystem = rr.CPE.TargetSoftware
+ }
+
+ var ranges []string
+ for _, ra := range rr.Detail.Ranges {
+ ranges = append(ranges, ra.Version.Constraint)
+ }
+ rangeStr := strings.Join(ranges, " || ")
+ rows = append(rows, []string{rr.Vulnerability.ID, pkgOrCPE, ecosystem, mimicV5Namespace(rr), rangeStr})
+ }
+
+ // sort rows by each column
+ sort.Slice(rows, func(i, j int) bool {
+ for k := range rows[i] {
+ if rows[i][k] != rows[j][k] {
+ return rows[i][k] < rows[j][k]
+ }
+ }
+ return false
+ })
+
+ return rows
+}
+
+func mimicV5Namespace(row dbsearch.AffectedPackage) string {
+ return v6.MimicV5Namespace(&row.Vulnerability.Model, row.Model)
+}
diff --git a/cmd/grype/cli/commands/db_search_test.go b/cmd/grype/cli/commands/db_search_test.go
new file mode 100644
index 00000000000..203ab015896
--- /dev/null
+++ b/cmd/grype/cli/commands/db_search_test.go
@@ -0,0 +1,106 @@
+package commands
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/cmd/grype/cli/options"
+)
+
+func TestDBSearchMatchOptionsApplyArgs(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ expectedPackages []string
+ expectedVulnIDs []string
+ expectedErrMessage string
+ }{
+ {
+ name: "empty arguments",
+ args: []string{},
+ expectedPackages: []string{},
+ expectedVulnIDs: []string{},
+ },
+ {
+ name: "valid cpe",
+ args: []string{"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*"},
+ expectedPackages: []string{
+ "cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*",
+ },
+ expectedVulnIDs: []string{},
+ },
+ {
+ name: "valid purl",
+ args: []string{"pkg:npm/package-name@1.0.0"},
+ expectedPackages: []string{
+ "pkg:npm/package-name@1.0.0",
+ },
+ expectedVulnIDs: []string{},
+ },
+ {
+ name: "valid vulnerability IDs",
+ args: []string{"CVE-2023-0001", "GHSA-1234", "ALAS-2023-1234"},
+ expectedPackages: []string{},
+ expectedVulnIDs: []string{
+ "CVE-2023-0001",
+ "GHSA-1234",
+ "ALAS-2023-1234",
+ },
+ },
+ {
+ name: "mixed package and vulns",
+ args: []string{"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*", "CVE-2023-0001"},
+ expectedPackages: []string{
+ "cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*",
+ },
+ expectedVulnIDs: []string{
+ "CVE-2023-0001",
+ },
+ },
+ {
+ name: "plain package name",
+ args: []string{"package-name"},
+ expectedPackages: []string{
+ "package-name",
+ },
+ expectedVulnIDs: []string{},
+ },
+ {
+ name: "invalid PostLoad error for Package",
+ args: []string{"pkg:npm/package-name@1.0.0", "cpe:invalid"},
+ expectedPackages: []string{
+ "pkg:npm/package-name@1.0.0",
+ },
+ expectedErrMessage: "invalid CPE",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ opts := &dbSearchMatchOptions{
+ Vulnerability: options.DBSearchVulnerabilities{},
+ Package: options.DBSearchPackages{},
+ }
+
+ err := opts.applyArgs(tc.args)
+
+ if tc.expectedErrMessage != "" {
+ require.Error(t, err)
+ require.Contains(t, err.Error(), tc.expectedErrMessage)
+ return
+ }
+
+ require.NoError(t, err)
+ if d := cmp.Diff(tc.expectedPackages, opts.Package.Packages, cmpopts.EquateEmpty()); d != "" {
+ t.Errorf("unexpected package specifiers: %s", d)
+ }
+ if d := cmp.Diff(tc.expectedVulnIDs, opts.Vulnerability.VulnerabilityIDs, cmpopts.EquateEmpty()); d != "" {
+ t.Errorf("unexpected vulnerability specifiers: %s", d)
+ }
+
+ })
+ }
+}
diff --git a/cmd/grype/cli/commands/db_search_vuln.go b/cmd/grype/cli/commands/db_search_vuln.go
new file mode 100644
index 00000000000..7cb389cb18b
--- /dev/null
+++ b/cmd/grype/cli/commands/db_search_vuln.go
@@ -0,0 +1,245 @@
+package commands
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/go-multierror"
+ "github.com/scylladb/go-set/strset"
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch"
+ "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"
+ "github.com/anchore/grype/internal/bus"
+ "github.com/anchore/grype/internal/cvss"
+)
+
+type dbSearchVulnerabilityOptions struct {
+ Format options.DBSearchFormat `yaml:",inline" mapstructure:",squash"`
+ Vulnerability options.DBSearchVulnerabilities `yaml:",inline" mapstructure:",squash"`
+ Bounds options.DBSearchBounds `yaml:",inline" mapstructure:",squash"`
+
+ options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+}
+
+func DBSearchVulnerabilities(app clio.Application) *cobra.Command {
+ opts := &dbSearchVulnerabilityOptions{
+ Format: options.DefaultDBSearchFormat(),
+ Vulnerability: options.DBSearchVulnerabilities{
+ UseVulnIDFlag: false, // we input this through the args
+ },
+ Bounds: options.DefaultDBSearchBounds(),
+ DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
+ }
+
+ cmd := &cobra.Command{
+ Use: "vuln ID...",
+ Aliases: []string{"vulnerability", "vulnerabilities", "vulns"},
+ Short: "Search for vulnerabilities within the DB (supports DB schema v6+ only)",
+ Args: func(_ *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return fmt.Errorf("must specify at least one vulnerability ID")
+ }
+ opts.Vulnerability.VulnerabilityIDs = args
+ return nil
+ },
+ RunE: func(_ *cobra.Command, _ []string) (err error) {
+ return runDBSearchVulnerabilities(*opts)
+ },
+ }
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ Hidden *dbSearchVulnerabilityOptions `json:"-" yaml:"-" mapstructure:"-"`
+ *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
+}
+
+func runDBSearchVulnerabilities(opts dbSearchVulnerabilityOptions) error {
+ client, err := distribution.NewClient(opts.ToClientConfig())
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+
+ c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
+ if err != nil {
+ return fmt.Errorf("unable to create curator: %w", err)
+ }
+
+ reader, err := c.Reader()
+ if err != nil {
+ return fmt.Errorf("unable to get providers: %w", err)
+ }
+
+ if err := validateProvidersFilter(reader, opts.Vulnerability.Providers); err != nil {
+ return err
+ }
+
+ rows, err := dbsearch.FindVulnerabilities(reader, dbsearch.VulnerabilitiesOptions{
+ Vulnerability: opts.Vulnerability.Specs,
+ RecordLimit: opts.Bounds.RecordLimit,
+ })
+ if err != nil {
+ return err
+ }
+
+ sb := &strings.Builder{}
+ err = presentDBSearchVulnerabilities(opts.Format.Output, rows, sb)
+ rep := sb.String()
+ if rep != "" {
+ bus.Report(rep)
+ }
+
+ return err
+}
+
+func validateProvidersFilter(reader v6.Reader, providers []string) error {
+ if len(providers) == 0 {
+ return nil
+ }
+ availableProviders, err := reader.AllProviders()
+ if err != nil {
+ return fmt.Errorf("unable to get providers: %w", err)
+ }
+ activeProviders := strset.New()
+ for _, p := range availableProviders {
+ activeProviders.Add(p.ID)
+ }
+
+ provSet := strset.New(providers...)
+
+ diff := strset.Difference(provSet, activeProviders)
+ diffList := diff.List()
+ sort.Strings(diffList)
+ var errs error
+ for _, p := range diffList {
+ errs = multierror.Append(errs, fmt.Errorf("provider not found: %q", p))
+ }
+
+ return errs
+}
+
+func presentDBSearchVulnerabilities(outputFormat string, structuredRows []dbsearch.Vulnerability, output io.Writer) error {
+ switch outputFormat {
+ case tableOutputFormat:
+ if len(structuredRows) == 0 {
+ bus.Notify("No results found")
+ return nil
+ }
+
+ rows := renderDBSearchVulnerabilitiesTableRows(structuredRows)
+
+ table := newTable(output)
+
+ table.SetHeader([]string{"ID", "Provider", "Published", "Severity", "Reference"})
+ table.AppendBulk(rows)
+ table.Render()
+ case jsonOutputFormat:
+ if structuredRows == nil {
+ // always allocate the top level collection
+ structuredRows = []dbsearch.Vulnerability{}
+ }
+ enc := json.NewEncoder(output)
+ enc.SetEscapeHTML(false)
+ enc.SetIndent("", " ")
+ if err := enc.Encode(structuredRows); err != nil {
+ return fmt.Errorf("failed to encode diff information: %+v", err)
+ }
+ default:
+ return fmt.Errorf("unsupported output format: %s", outputFormat)
+ }
+ return nil
+}
+
+func renderDBSearchVulnerabilitiesTableRows(structuredRows []dbsearch.Vulnerability) [][]string {
+ type row struct {
+ Vuln string
+ ProviderWithoutVersions string
+ PublishedDate string
+ Severity string
+ Reference string
+ }
+
+ versionsByRow := make(map[row][]string)
+ for _, rr := range structuredRows {
+ r := row{
+ Vuln: rr.ID,
+ ProviderWithoutVersions: rr.Provider,
+ PublishedDate: getDate(rr.PublishedDate),
+ Severity: getSeverity(rr.Severities),
+ Reference: getPrimaryReference(rr.References),
+ }
+ versionsByRow[r] = append(versionsByRow[r], getOSVersions(rr.OperatingSystems)...)
+ }
+
+ var rows [][]string
+ for r, versions := range versionsByRow {
+ prov := r.ProviderWithoutVersions
+ if len(versions) > 0 {
+ sort.Strings(versions)
+ prov = fmt.Sprintf("%s (%s)", r.ProviderWithoutVersions, strings.Join(versions, ", "))
+ }
+ rows = append(rows, []string{r.Vuln, prov, r.PublishedDate, r.Severity, r.Reference})
+ }
+
+ // sort rows by each column
+ sort.Slice(rows, func(i, j int) bool {
+ for k := range rows[i] {
+ if rows[i][k] != rows[j][k] {
+ return rows[i][k] < rows[j][k]
+ }
+ }
+ return false
+ })
+
+ 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
new file mode 100644
index 00000000000..af402e27aab
--- /dev/null
+++ b/cmd/grype/cli/commands/db_status.go
@@ -0,0 +1,103 @@
+package commands
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/options"
+ "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 {
+ Output string `yaml:"output" json:"output" mapstructure:"output"`
+ options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+}
+
+var _ clio.FlagAdder = (*dbStatusOptions)(nil)
+
+func (d *dbStatusOptions) AddFlags(flags clio.FlagSet) {
+ flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, json])")
+}
+
+func DBStatus(app clio.Application) *cobra.Command {
+ opts := &dbStatusOptions{
+ Output: textOutputFormat,
+ DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
+ }
+
+ cmd := &cobra.Command{
+ Use: "status",
+ Short: "Display database status and metadata",
+ Args: cobra.ExactArgs(0),
+ PreRunE: disableUI(app),
+ RunE: func(_ *cobra.Command, _ []string) error {
+ return runDBStatus(*opts)
+ },
+ }
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ Hidden *dbStatusOptions `json:"-" yaml:"-" mapstructure:"-"`
+ *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
+}
+
+func runDBStatus(opts dbStatusOptions) error {
+ client, err := distribution.NewClient(opts.ToClientConfig())
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+ c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+
+ status := c.Status()
+
+ if err := presentDBStatus(opts.Output, os.Stdout, status); err != nil {
+ return fmt.Errorf("failed to present db status information: %+v", err)
+ }
+
+ return 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.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)
+ enc.SetIndent("", " ")
+ if err := enc.Encode(&status); err != nil {
+ return fmt.Errorf("failed to db status information: %+v", err)
+ }
+ default:
+ return fmt.Errorf("unsupported output format: %s", format)
+ }
+
+ 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
new file mode 100644
index 00000000000..a2d774a8b4c
--- /dev/null
+++ b/cmd/grype/cli/commands/db_status_test.go
@@ -0,0 +1,117 @@
+package commands
+
+import (
+ "bytes"
+ "errors"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+func TestPresentDBStatus(t *testing.T) {
+ 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: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC),
+ Error: nil,
+ }
+
+ 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: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC),
+ Error: errors.New("checksum mismatch"),
+ }
+
+ tests := []struct {
+ name string
+ format string
+ status vulnerability.ProviderStatus
+ expectedText string
+ expectedErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "valid status, text format",
+ format: textOutputFormat,
+ status: validStatus,
+ expectedText: `Path: /Users/test/Library/Caches/grype/db/6/vulnerability.db
+Schema: 6.0.0
+Built: 2024-11-27T14:43:17Z
+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,
+ },
+ {
+ name: "invalid status, text format",
+ format: textOutputFormat,
+ status: invalidStatus,
+ expectedText: `Path: /Users/test/Library/Caches/grype/db/6/vulnerability.db
+Schema: 6.0.0
+Built: 2024-11-27T14:43:17Z
+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,
+ },
+ {
+ name: "valid status, JSON format",
+ format: jsonOutputFormat,
+ 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",
+ "valid": true
+}
+`,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "invalid status, JSON format",
+ format: jsonOutputFormat,
+ 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",
+ "valid": false,
+ "error": "checksum mismatch"
+}
+`,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "unsupported format",
+ format: "unsupported",
+ status: validStatus,
+ expectedErr: requireErrorContains("unsupported output format"),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectedErr == nil {
+ tt.expectedErr = require.NoError
+ }
+ writer := &bytes.Buffer{}
+
+ err := presentDBStatus(tt.format, writer, tt.status)
+ tt.expectedErr(t, err)
+ if err != nil {
+ return
+ }
+
+ assert.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(writer.String()))
+ })
+ }
+}
diff --git a/cmd/grype/cli/commands/db_update.go b/cmd/grype/cli/commands/db_update.go
new file mode 100644
index 00000000000..10745f3e0fc
--- /dev/null
+++ b/cmd/grype/cli/commands/db_update.go
@@ -0,0 +1,73 @@
+package commands
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/options"
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ "github.com/anchore/grype/grype/db/v6/installation"
+ "github.com/anchore/grype/internal/bus"
+ "github.com/anchore/grype/internal/log"
+)
+
+func DBUpdate(app clio.Application) *cobra.Command {
+ opts := options.DefaultDatabaseCommand(app.ID())
+
+ cmd := &cobra.Command{
+ Use: "update",
+ Short: "Download and install the latest vulnerability database",
+ Args: cobra.ExactArgs(0),
+ PreRunE: func(_ *cobra.Command, _ []string) error {
+ // DB commands should not opt into the low-pass check filter
+ opts.DB.MaxUpdateCheckFrequency = 0
+ return nil
+ },
+ RunE: func(_ *cobra.Command, _ []string) error {
+ return runDBUpdate(*opts)
+ },
+ }
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{opts})
+}
+
+func runDBUpdate(opts options.DatabaseCommand) error {
+ cfg := opts.ToClientConfig()
+ // we need to have this set to true to force the update call to try to update
+ // regardless of what the user provided in order for update checks to fail
+ if !cfg.RequireUpdateCheck {
+ log.Warn("overriding db update check")
+ cfg.RequireUpdateCheck = true
+ }
+ client, err := distribution.NewClient(cfg)
+ if err != nil {
+ return fmt.Errorf("unable to create distribution client: %w", err)
+ }
+ c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
+ if err != nil {
+ return fmt.Errorf("unable to create curator: %w", err)
+ }
+
+ updated, err := c.Update()
+ if err != nil {
+ return fmt.Errorf("unable to update vulnerability database: %w", err)
+ }
+
+ result := "No vulnerability database update available\n"
+ if updated {
+ result = "Vulnerability database updated to latest version!\n"
+ }
+
+ log.Debugf("completed db update check with result: %s", result)
+
+ bus.Report(result)
+
+ return nil
+}
diff --git a/cmd/grype/cli/commands/explain.go b/cmd/grype/cli/commands/explain.go
new file mode 100644
index 00000000000..36031b46654
--- /dev/null
+++ b/cmd/grype/cli/commands/explain.go
@@ -0,0 +1,64 @@
+package commands
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/grype/presenter/explain"
+ "github.com/anchore/grype/grype/presenter/models"
+ "github.com/anchore/grype/internal"
+ "github.com/anchore/grype/internal/log"
+)
+
+type explainOptions struct {
+ CVEIDs []string `yaml:"cve-ids" json:"cve-ids" mapstructure:"cve-ids"`
+}
+
+var _ clio.FlagAdder = (*explainOptions)(nil)
+
+func (d *explainOptions) AddFlags(flags clio.FlagSet) {
+ flags.StringArrayVarP(&d.CVEIDs, "id", "", "CVE IDs to explain")
+}
+
+func Explain(app clio.Application) *cobra.Command {
+ opts := &explainOptions{}
+
+ cmd := &cobra.Command{
+ Use: "explain --id [VULNERABILITY ID]",
+ Short: "Ask grype to explain a set of findings",
+ PreRunE: disableUI(app),
+ RunE: func(_ *cobra.Command, _ []string) error {
+ log.Warn("grype explain is a prototype feature and is subject to change")
+ isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect()
+ if err != nil {
+ log.Warnf("unable to determine if there is piped input: %+v", err)
+ isStdinPipeOrRedirect = false
+ }
+ if isStdinPipeOrRedirect {
+ // TODO: eventually detect different types of input; for now assume grype json
+ var parseResult models.Document
+ decoder := json.NewDecoder(os.Stdin)
+ err := decoder.Decode(&parseResult)
+ if err != nil {
+ return fmt.Errorf("unable to parse piped input: %+v", err)
+ }
+ explainer := explain.NewVulnerabilityExplainer(os.Stdout, &parseResult)
+ return explainer.ExplainByID(opts.CVEIDs)
+ }
+ // perform a scan, then explain requested CVEs
+ // TODO: implement
+ return fmt.Errorf("requires grype json on stdin, please run 'grype -o json ... | grype explain ...'")
+ },
+ }
+
+ // prevent from being shown in the grype config
+ type configWrapper struct {
+ Opts *explainOptions `json:"-" yaml:"-" mapstructure:"-"`
+ }
+
+ return app.SetupCommand(cmd, &configWrapper{opts})
+}
diff --git a/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go b/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go
new file mode 100644
index 00000000000..17af2ba64d7
--- /dev/null
+++ b/cmd/grype/cli/commands/internal/dbsearch/affected_packages.go
@@ -0,0 +1,309 @@
+package dbsearch
+
+import (
+ "errors"
+ "fmt"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/syft/syft/cpe"
+)
+
+var ErrNoSearchCriteria = errors.New("must provide at least one of vulnerability or package to search for")
+
+// AffectedPackage represents a package affected by a vulnerability
+type AffectedPackage struct {
+ // Vulnerability is the core advisory record for a single known vulnerability from a specific provider.
+ Vulnerability VulnerabilityInfo `json:"vulnerability"`
+
+ // AffectedPackageInfo is the detailed information about the affected package
+ AffectedPackageInfo `json:",inline"`
+}
+
+type AffectedPackageInfo struct {
+ // TODO: remove this when namespace is no longer used
+ Model *v6.AffectedPackageHandle `json:"-"` // tracking package handle info is necessary for namespace lookup (note CPE handles are not tracked)
+
+ // OS identifies the operating system release that the affected package is released for
+ OS *OperatingSystem `json:"os,omitempty"`
+
+ // Package identifies the name of the package in a specific ecosystem affected by the vulnerability
+ Package *Package `json:"package,omitempty"`
+
+ // 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"`
+}
+
+// Package represents a package name within a known ecosystem, such as "python" or "golang".
+type Package struct {
+
+ // Name is the name of the package within the ecosystem
+ Name string `json:"name"`
+
+ // Ecosystem is the tooling and language ecosystem that the package is released within
+ Ecosystem string `json:"ecosystem"`
+}
+
+// CPE is a Common Platform Enumeration that identifies a package
+type CPE v6.Cpe
+
+func (c *CPE) MarshalJSON() ([]byte, error) {
+ return []byte(fmt.Sprintf("%q", c.String())), nil
+}
+
+func (c *CPE) String() string {
+ if c == nil {
+ return ""
+ }
+
+ return v6.Cpe(*c).String()
+}
+
+type AffectedPackagesOptions struct {
+ Vulnerability v6.VulnerabilitySpecifiers
+ Package v6.PackageSpecifiers
+ CPE v6.PackageSpecifiers
+ OS v6.OSSpecifiers
+ AllowBroadCPEMatching bool
+ RecordLimit int
+}
+
+type affectedPackageWithDecorations struct {
+ v6.AffectedPackageHandle
+ vulnerabilityDecorations
+}
+
+func (a *affectedPackageWithDecorations) getCVEs() []string {
+ if a == nil {
+ return nil
+ }
+ return getCVEs(a.Vulnerability)
+}
+
+type affectedCPEWithDecorations struct {
+ v6.AffectedCPEHandle
+ vulnerabilityDecorations
+}
+
+func (a *affectedCPEWithDecorations) getCVEs() []string {
+ if a == nil {
+ return nil
+ }
+ return getCVEs(a.Vulnerability)
+}
+
+func newAffectedPackageRows(affectedPkgs []affectedPackageWithDecorations, affectedCPEs []affectedCPEWithDecorations) (rows []AffectedPackage) {
+ for i := range affectedPkgs {
+ pkg := affectedPkgs[i]
+ var detail v6.AffectedPackageBlob
+ if pkg.BlobValue != nil {
+ detail = *pkg.BlobValue
+ }
+ if pkg.Vulnerability == nil {
+ log.Errorf("affected package record missing vulnerability: %+v", pkg)
+ continue
+ }
+
+ rows = append(rows, AffectedPackage{
+ Vulnerability: newVulnerabilityInfo(*pkg.Vulnerability, pkg.vulnerabilityDecorations),
+ AffectedPackageInfo: AffectedPackageInfo{
+ Model: &pkg.AffectedPackageHandle,
+ OS: toOS(pkg.OperatingSystem),
+ Package: toPackage(pkg.Package),
+ Namespace: v6.MimicV5Namespace(pkg.Vulnerability, &pkg.AffectedPackageHandle),
+ Detail: detail,
+ },
+ })
+ }
+
+ for _, ac := range affectedCPEs {
+ var detail v6.AffectedPackageBlob
+ if ac.BlobValue != nil {
+ detail = *ac.BlobValue
+ }
+ if ac.Vulnerability == nil {
+ log.Errorf("affected CPE record missing vulnerability: %+v", ac)
+ continue
+ }
+
+ var c *CPE
+ if ac.CPE != nil {
+ cv := CPE(*ac.CPE)
+ c = &cv
+ }
+
+ rows = append(rows, AffectedPackage{
+ // tracking model information is not possible with CPE handles
+ Vulnerability: newVulnerabilityInfo(*ac.Vulnerability, ac.vulnerabilityDecorations),
+ AffectedPackageInfo: AffectedPackageInfo{
+ CPE: c,
+ Namespace: v6.MimicV5Namespace(ac.Vulnerability, nil), // no affected package will default to NVD
+ Detail: detail,
+ },
+ })
+ }
+
+ return rows
+}
+
+func toPackage(pkg *v6.Package) *Package {
+ if pkg == nil {
+ return nil
+ }
+ return &Package{
+ Name: pkg.Name,
+ Ecosystem: pkg.Ecosystem,
+ }
+}
+
+func toOS(os *v6.OperatingSystem) *OperatingSystem {
+ if os == nil {
+ return nil
+ }
+ version := os.VersionNumber()
+ if version == "" {
+ version = os.Version()
+ }
+
+ return &OperatingSystem{
+ Name: os.Name,
+ Version: version,
+ }
+}
+
+func FindAffectedPackages(reader interface {
+ v6.AffectedPackageStoreReader
+ v6.AffectedCPEStoreReader
+ v6.VulnerabilityDecoratorStoreReader
+}, criteria AffectedPackagesOptions) ([]AffectedPackage, error) {
+ allAffectedPkgs, allAffectedCPEs, err := findAffectedPackages(reader, criteria)
+ if err != nil {
+ return nil, err
+ }
+
+ return newAffectedPackageRows(allAffectedPkgs, allAffectedCPEs), nil
+}
+
+func findAffectedPackages(reader interface { //nolint:funlen,gocognit
+ v6.AffectedPackageStoreReader
+ v6.AffectedCPEStoreReader
+ v6.VulnerabilityDecoratorStoreReader
+}, config AffectedPackagesOptions) ([]affectedPackageWithDecorations, []affectedCPEWithDecorations, error) {
+ var allAffectedPkgs []affectedPackageWithDecorations
+ var allAffectedCPEs []affectedCPEWithDecorations
+
+ pkgSpecs := config.Package
+ cpeSpecs := config.CPE
+ osSpecs := config.OS
+ vulnSpecs := config.Vulnerability
+
+ if config.RecordLimit == 0 {
+ log.Warn("no record limit set! For queries with large result sets this may result in performance issues")
+ }
+
+ if len(vulnSpecs) == 0 && len(pkgSpecs) == 0 && len(cpeSpecs) == 0 {
+ return nil, nil, ErrNoSearchCriteria
+ }
+
+ // don't allow for searching by any package AND any CPE AND any vulnerability AND any OS. Since these searches
+ // are oriented by primarily package, we only want to have ANY package/CPE when there is a vulnerability or OS specified.
+ if len(vulnSpecs) > 0 || !osSpecs.IsAny() {
+ if len(pkgSpecs) == 0 {
+ pkgSpecs = []*v6.PackageSpecifier{v6.AnyPackageSpecified}
+ }
+
+ if len(cpeSpecs) == 0 {
+ cpeSpecs = []*v6.PackageSpecifier{v6.AnyPackageSpecified}
+ }
+ }
+
+ // we have multiple return points that return actual values, using a defer to decorate any given results
+ // ensures that all paths are handled the same way.
+ defer func() {
+ for i := range allAffectedPkgs {
+ if err := decorateVulnerabilities(reader, &allAffectedPkgs[i]); err != nil {
+ log.WithFields("error", err).Debug("unable to decorate vulnerability on affected package")
+ }
+ }
+
+ for i := range allAffectedCPEs {
+ if err := decorateVulnerabilities(reader, &allAffectedCPEs[i]); err != nil {
+ log.WithFields("error", err).Debug("unable to decorate vulnerability on affected CPE")
+ }
+ }
+ }()
+
+ for i := range pkgSpecs {
+ pkgSpec := pkgSpecs[i]
+
+ log.WithFields("vuln", vulnSpecs, "pkg", pkgSpec, "os", osSpecs).Debug("searching for affected packages")
+
+ affectedPkgs, err := reader.GetAffectedPackages(pkgSpec, &v6.GetAffectedPackageOptions{
+ PreloadOS: true,
+ PreloadPackage: true,
+ PreloadPackageCPEs: false,
+ PreloadVulnerability: true,
+ PreloadBlob: true,
+ OSs: osSpecs,
+ Vulnerabilities: vulnSpecs,
+ AllowBroadCPEMatching: config.AllowBroadCPEMatching,
+ Limit: config.RecordLimit,
+ })
+
+ for i := range affectedPkgs {
+ allAffectedPkgs = append(allAffectedPkgs, affectedPackageWithDecorations{
+ AffectedPackageHandle: affectedPkgs[i],
+ })
+ }
+
+ if err != nil {
+ if errors.Is(err, v6.ErrLimitReached) {
+ return allAffectedPkgs, allAffectedCPEs, err
+ }
+ return nil, nil, fmt.Errorf("unable to get affected packages for %s: %w", vulnSpecs, err)
+ }
+ }
+
+ if osSpecs.IsAny() {
+ for i := range cpeSpecs {
+ cpeSpec := cpeSpecs[i]
+ var searchCPE *cpe.Attributes
+ if cpeSpec != nil {
+ searchCPE = cpeSpec.CPE
+ }
+
+ log.WithFields("vuln", vulnSpecs, "cpe", cpeSpec).Debug("searching for affected packages")
+
+ affectedCPEs, err := reader.GetAffectedCPEs(searchCPE, &v6.GetAffectedCPEOptions{
+ PreloadCPE: true,
+ PreloadVulnerability: true,
+ PreloadBlob: true,
+ Vulnerabilities: vulnSpecs,
+ AllowBroadCPEMatching: config.AllowBroadCPEMatching,
+ Limit: config.RecordLimit,
+ })
+
+ for i := range affectedCPEs {
+ allAffectedCPEs = append(allAffectedCPEs, affectedCPEWithDecorations{
+ AffectedCPEHandle: affectedCPEs[i],
+ })
+ }
+
+ if err != nil {
+ if errors.Is(err, v6.ErrLimitReached) {
+ return allAffectedPkgs, allAffectedCPEs, err
+ }
+ return nil, nil, fmt.Errorf("unable to get affected cpes for %s: %w", vulnSpecs, err)
+ }
+ }
+ }
+
+ return allAffectedPkgs, allAffectedCPEs, nil
+}
diff --git a/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go b/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go
new file mode 100644
index 00000000000..5c6442f9581
--- /dev/null
+++ b/cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go
@@ -0,0 +1,863 @@
+package dbsearch
+
+import (
+ "bytes"
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/syft/syft/cpe"
+)
+
+func TestAffectedPackageTableRowMarshalJSON(t *testing.T) {
+ row := AffectedPackage{
+ Vulnerability: VulnerabilityInfo{
+ VulnerabilityBlob: v6.VulnerabilityBlob{
+ ID: "CVE-1234-5678",
+ Description: "Test vulnerability",
+ },
+ 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)),
+ KnownExploited: []KnownExploited{
+ {
+ CVE: "CVE-1234-5678",
+ VendorProject: "LinuxFoundation",
+ Product: "Linux",
+ DateAdded: "2025-02-02",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-02",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-1234"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-1234-5678",
+ EPSS: 0.893,
+ Percentile: 0.99,
+ Date: "2025-02-24",
+ },
+ },
+ },
+ AffectedPackageInfo: AffectedPackageInfo{
+ 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{
+ RpmModularity: ptr("modularity"),
+ PlatformCPEs: []string{"platform-cpe-1"},
+ },
+ Ranges: []v6.AffectedRange{
+ {
+ Version: v6.AffectedVersion{
+ Type: "semver",
+ Constraint: ">=1.0.0, <2.0.0",
+ },
+ Fix: &v6.Fix{
+ Version: "1.2.0",
+ State: "fixed",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ buf := bytes.Buffer{}
+ enc := json.NewEncoder(&buf)
+ enc.SetIndent("", " ")
+ enc.SetEscapeHTML(false)
+ err := enc.Encode(row)
+ require.NoError(t, err)
+
+ expectedJSON := `{
+ "vulnerability": {
+ "id": "CVE-1234-5678",
+ "description": "Test vulnerability",
+ "provider": "provider1",
+ "status": "active",
+ "published_date": "2023-01-01T00:00:00Z",
+ "modified_date": "2023-02-01T00:00:00Z",
+ "known_exploited": [
+ {
+ "cve": "CVE-1234-5678",
+ "vendor_project": "LinuxFoundation",
+ "product": "Linux",
+ "date_added": "2025-02-02",
+ "required_action": "Yes",
+ "due_date": "2025-02-02",
+ "known_ransomware_campaign_use": "Known",
+ "notes": "note!",
+ "urls": [
+ "https://example.com"
+ ],
+ "cwes": [
+ "CWE-1234"
+ ]
+ }
+ ],
+ "epss": [
+ {
+ "cve": "CVE-1234-5678",
+ "epss": 0.893,
+ "percentile": 0.99,
+ "date": "2025-02-24"
+ }
+ ]
+ },
+ "package": {
+ "name": "pkg1",
+ "ecosystem": "ecosystem1"
+ },
+ "cpe": "cpe:2.3:a:vendor1:product1:*:*:*:*:*:*",
+ "namespace": "namespace1",
+ "detail": {
+ "cves": [
+ "CVE-1234-5678"
+ ],
+ "qualifiers": {
+ "rpm_modularity": "modularity",
+ "platform_cpes": [
+ "platform-cpe-1"
+ ]
+ },
+ "ranges": [
+ {
+ "version": {
+ "type": "semver",
+ "constraint": ">=1.0.0, <2.0.0"
+ },
+ "fix": {
+ "version": "1.2.0",
+ "state": "fixed"
+ }
+ }
+ ]
+ }
+}
+`
+
+ if diff := cmp.Diff(expectedJSON, buf.String()); diff != "" {
+ t.Errorf("unexpected JSON (-want +got):\n%s", diff)
+ }
+}
+
+func TestNewAffectedPackageRows(t *testing.T) {
+ affectedPkgs := []affectedPackageWithDecorations{
+ {
+ AffectedPackageHandle: v6.AffectedPackageHandle{
+ Package: &v6.Package{Name: "pkg1", Ecosystem: "ecosystem1"},
+ OperatingSystem: &v6.OperatingSystem{
+ Name: "Linux",
+ MajorVersion: "5",
+ MinorVersion: "10",
+ },
+ Vulnerability: &v6.VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ Provider: &v6.Provider{ID: "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)),
+ BlobValue: &v6.VulnerabilityBlob{Description: "Test vulnerability"},
+ },
+ BlobValue: &v6.AffectedPackageBlob{
+ CVEs: []string{"CVE-1234-5678"},
+ Qualifiers: &v6.AffectedPackageQualifiers{
+ RpmModularity: ptr("modularity"),
+ PlatformCPEs: []string{"platform-cpe-1"},
+ },
+ Ranges: []v6.AffectedRange{
+ {
+ Version: v6.AffectedVersion{
+ Type: "semver",
+ Constraint: ">=1.0.0, <2.0.0",
+ },
+ Fix: &v6.Fix{
+ Version: "1.2.0",
+ State: "fixed",
+ },
+ },
+ },
+ },
+ },
+ vulnerabilityDecorations: vulnerabilityDecorations{
+ KnownExploited: []KnownExploited{
+ {
+ CVE: "CVE-1234-5678",
+ VendorProject: "LinuxFoundation",
+ Product: "Linux",
+ DateAdded: "2025-02-02",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-02",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-1234"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-1234-5678",
+ EPSS: 0.893,
+ Percentile: 0.99,
+ Date: "2025-02-24",
+ },
+ },
+ },
+ },
+ }
+
+ affectedCPEs := []affectedCPEWithDecorations{
+ {
+ AffectedCPEHandle: v6.AffectedCPEHandle{
+ CPE: &v6.Cpe{Part: "a", Vendor: "vendor1", Product: "product1"},
+ Vulnerability: &v6.VulnerabilityHandle{
+ Name: "CVE-9876-5432",
+ Provider: &v6.Provider{ID: "provider2"},
+ BlobValue: &v6.VulnerabilityBlob{Description: "CPE vulnerability description"},
+ },
+ BlobValue: &v6.AffectedPackageBlob{
+ CVEs: []string{"CVE-9876-5432"},
+ Ranges: []v6.AffectedRange{
+ {
+ Version: v6.AffectedVersion{
+ Type: "rpm",
+ Constraint: ">=2.0.0, <3.0.0",
+ },
+ Fix: &v6.Fix{
+ Version: "2.5.0",
+ State: "fixed",
+ },
+ },
+ },
+ },
+ },
+ vulnerabilityDecorations: vulnerabilityDecorations{
+ KnownExploited: []KnownExploited{
+ {
+ CVE: "CVE-9876-5432",
+ VendorProject: "vendor1",
+ Product: "product1",
+ DateAdded: "2025-02-03",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-03",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-5678"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-9876-5432",
+ EPSS: 0.938,
+ Percentile: 0.9222,
+ Date: "2025-02-25",
+ },
+ },
+ },
+ },
+ }
+
+ rows := newAffectedPackageRows(affectedPkgs, affectedCPEs)
+ expected := []AffectedPackage{
+ {
+ Vulnerability: VulnerabilityInfo{
+ VulnerabilityBlob: v6.VulnerabilityBlob{Description: "Test vulnerability"},
+ 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)),
+ KnownExploited: []KnownExploited{
+ {
+ CVE: "CVE-1234-5678",
+ VendorProject: "LinuxFoundation",
+ Product: "Linux",
+ DateAdded: "2025-02-02",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-02",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-1234"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-1234-5678",
+ EPSS: 0.893,
+ Percentile: 0.99,
+ Date: "2025-02-24",
+ },
+ },
+ },
+ AffectedPackageInfo: AffectedPackageInfo{
+ 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{
+ RpmModularity: ptr("modularity"),
+ PlatformCPEs: []string{"platform-cpe-1"},
+ },
+ Ranges: []v6.AffectedRange{
+ {
+ Version: v6.AffectedVersion{
+ Type: "semver",
+ Constraint: ">=1.0.0, <2.0.0",
+ },
+ Fix: &v6.Fix{
+ Version: "1.2.0",
+ State: "fixed",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Vulnerability: VulnerabilityInfo{
+ VulnerabilityBlob: v6.VulnerabilityBlob{Description: "CPE vulnerability description"},
+ Provider: "provider2",
+ KnownExploited: []KnownExploited{
+ {
+ CVE: "CVE-9876-5432",
+ VendorProject: "vendor1",
+ Product: "product1",
+ DateAdded: "2025-02-03",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-03",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-5678"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-9876-5432",
+ EPSS: 0.938,
+ Percentile: 0.9222,
+ Date: "2025-02-25",
+ },
+ },
+ },
+ AffectedPackageInfo: AffectedPackageInfo{
+ CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"},
+ Namespace: "provider2:cpe",
+ Detail: v6.AffectedPackageBlob{
+ CVEs: []string{"CVE-9876-5432"},
+ Ranges: []v6.AffectedRange{
+ {
+ Version: v6.AffectedVersion{
+ Type: "rpm",
+ Constraint: ">=2.0.0, <3.0.0",
+ },
+ Fix: &v6.Fix{
+ Version: "2.5.0",
+ State: "fixed",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ if diff := cmp.Diff(expected, rows, cmpOpts()...); diff != "" {
+ t.Errorf("unexpected rows (-want +got):\n%s", diff)
+ }
+}
+
+func TestAffectedPackages(t *testing.T) {
+ mockReader := new(affectedMockReader)
+
+ mockReader.On("GetAffectedPackages", mock.Anything, mock.Anything).Return([]v6.AffectedPackageHandle{
+ {
+ Package: &v6.Package{Name: "pkg1", Ecosystem: "ecosystem1"},
+ OperatingSystem: &v6.OperatingSystem{
+ Name: "Linux",
+ MajorVersion: "5",
+ MinorVersion: "10",
+ },
+ Vulnerability: &v6.VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ Provider: &v6.Provider{ID: "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)),
+ BlobValue: &v6.VulnerabilityBlob{Description: "Test vulnerability"},
+ },
+ BlobValue: &v6.AffectedPackageBlob{
+ CVEs: []string{"CVE-1234-5678"},
+ Ranges: []v6.AffectedRange{
+ {
+ Version: v6.AffectedVersion{
+ Type: "semver",
+ Constraint: ">=1.0.0, <2.0.0",
+ },
+ Fix: &v6.Fix{
+ Version: "1.2.0",
+ State: "fixed",
+ },
+ },
+ },
+ },
+ },
+ }, nil)
+
+ mockReader.On("GetAffectedCPEs", mock.Anything, mock.Anything).Return([]v6.AffectedCPEHandle{
+ {
+ CPE: &v6.Cpe{Part: "a", Vendor: "vendor1", Product: "product1"},
+ Vulnerability: &v6.VulnerabilityHandle{
+ Name: "CVE-9876-5432",
+ Provider: &v6.Provider{ID: "provider2"},
+ BlobValue: &v6.VulnerabilityBlob{Description: "CPE vulnerability description"},
+ },
+ BlobValue: &v6.AffectedPackageBlob{
+ CVEs: []string{"CVE-9876-5432"},
+ Ranges: []v6.AffectedRange{
+ {
+ Version: v6.AffectedVersion{
+ Type: "rpm",
+ Constraint: ">=2.0.0, <3.0.0",
+ },
+ Fix: &v6.Fix{
+ Version: "2.5.0",
+ State: "fixed",
+ },
+ },
+ },
+ },
+ },
+ }, nil)
+
+ mockReader.On("GetKnownExploitedVulnerabilities", "CVE-1234-5678").Return([]v6.KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-1234-5678",
+ BlobValue: &v6.KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-1234-5678",
+ VendorProject: "LinuxFoundation",
+ Product: "Linux",
+ DateAdded: ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)),
+ RequiredAction: "Yes",
+ DueDate: ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)),
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-1234"},
+ },
+ },
+ }, nil)
+
+ mockReader.On("GetKnownExploitedVulnerabilities", "CVE-9876-5432").Return([]v6.KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-9876-5432",
+ BlobValue: &v6.KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-9876-5432",
+ VendorProject: "vendor1",
+ Product: "product1",
+ DateAdded: ptr(time.Date(2025, 2, 3, 0, 0, 0, 0, time.UTC)),
+ RequiredAction: "Yes",
+ DueDate: ptr(time.Date(2025, 2, 3, 0, 0, 0, 0, time.UTC)),
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-5678"},
+ },
+ },
+ }, nil)
+
+ mockReader.On("GetEpss", "CVE-1234-5678").Return([]v6.EpssHandle{
+ {
+ Cve: "CVE-1234-5678",
+ Epss: 0.893,
+ Percentile: 0.99,
+ Date: time.Date(2025, 2, 24, 0, 0, 0, 0, time.UTC),
+ },
+ }, nil)
+
+ mockReader.On("GetEpss", "CVE-9876-5432").Return([]v6.EpssHandle{
+ {
+ Cve: "CVE-9876-5432",
+ Epss: 0.938,
+ Percentile: 0.9222,
+ Date: time.Date(2025, 2, 25, 0, 0, 0, 0, time.UTC),
+ },
+ }, nil)
+
+ criteria := AffectedPackagesOptions{
+ Vulnerability: v6.VulnerabilitySpecifiers{
+ {Name: "CVE-1234-5678"},
+ },
+ }
+
+ results, err := FindAffectedPackages(mockReader, criteria)
+ require.NoError(t, err)
+
+ expected := []AffectedPackage{
+ {
+ Vulnerability: VulnerabilityInfo{
+ VulnerabilityBlob: v6.VulnerabilityBlob{Description: "Test vulnerability"},
+ 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)),
+ KnownExploited: []KnownExploited{
+ {
+ CVE: "CVE-1234-5678",
+ VendorProject: "LinuxFoundation",
+ Product: "Linux",
+ DateAdded: "2025-02-02",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-02",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-1234"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-1234-5678",
+ EPSS: 0.893,
+ Percentile: 0.99,
+ Date: "2025-02-24",
+ },
+ },
+ },
+ AffectedPackageInfo: AffectedPackageInfo{
+ 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{
+ {
+ Version: v6.AffectedVersion{
+ Type: "semver",
+ Constraint: ">=1.0.0, <2.0.0",
+ },
+ Fix: &v6.Fix{
+ Version: "1.2.0",
+ State: "fixed",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Vulnerability: VulnerabilityInfo{
+ VulnerabilityBlob: v6.VulnerabilityBlob{Description: "CPE vulnerability description"},
+ Provider: "provider2",
+ KnownExploited: []KnownExploited{
+ {
+ CVE: "CVE-9876-5432",
+ VendorProject: "vendor1",
+ Product: "product1",
+ DateAdded: "2025-02-03",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-03",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-5678"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-9876-5432",
+ EPSS: 0.938,
+ Percentile: 0.9222,
+ Date: "2025-02-25",
+ },
+ },
+ },
+ AffectedPackageInfo: AffectedPackageInfo{
+ CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"},
+ Namespace: "provider2:cpe",
+ Detail: v6.AffectedPackageBlob{
+ CVEs: []string{"CVE-9876-5432"},
+ Ranges: []v6.AffectedRange{
+ {
+ Version: v6.AffectedVersion{
+ Type: "rpm",
+ Constraint: ">=2.0.0, <3.0.0",
+ },
+ Fix: &v6.Fix{
+ Version: "2.5.0",
+ State: "fixed",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ if diff := cmp.Diff(expected, results, cmpOpts()...); diff != "" {
+ t.Errorf("unexpected results (-want +got):\n%s", diff)
+ }
+}
+
+func TestFindAffectedPackages(t *testing.T) {
+ // this test is not meant to check the correctness of the results relative to the reader but instead make certain
+ // that the correct calls are made to the reader based on the search criteria (we're wired up correctly).
+ // Additional verifications are made to check that the combinations of different specs are handled correctly.
+ type pkgCall struct {
+ pkg *v6.PackageSpecifier
+ options *v6.GetAffectedPackageOptions
+ }
+
+ type cpeCall struct {
+ cpe *cpe.Attributes
+ options *v6.GetAffectedCPEOptions
+ }
+
+ testCases := []struct {
+ name string
+ config AffectedPackagesOptions
+ expectedPkgCalls []pkgCall
+ expectedCPECalls []cpeCall
+ expectedErr error
+ }{
+ {
+ name: "no search criteria",
+ config: AffectedPackagesOptions{},
+ expectedErr: ErrNoSearchCriteria,
+ },
+ {
+ name: "os spec alone is not enough",
+ config: AffectedPackagesOptions{
+ OS: v6.OSSpecifiers{
+ {Name: "ubuntu", MajorVersion: "20", MinorVersion: "04"},
+ },
+ },
+ expectedErr: ErrNoSearchCriteria,
+ },
+ {
+ name: "vuln spec provided",
+ config: AffectedPackagesOptions{
+ Vulnerability: v6.VulnerabilitySpecifiers{
+ {Name: "CVE-2023-0001"},
+ },
+ },
+ expectedPkgCalls: []pkgCall{
+ {
+ pkg: nil,
+ options: &v6.GetAffectedPackageOptions{
+ PreloadOS: true,
+ PreloadPackage: true,
+ PreloadVulnerability: true,
+ PreloadBlob: true,
+ Vulnerabilities: v6.VulnerabilitySpecifiers{
+ {Name: "CVE-2023-0001"},
+ },
+ Limit: 0,
+ },
+ },
+ },
+ expectedCPECalls: []cpeCall{
+ {
+ cpe: nil,
+ options: &v6.GetAffectedCPEOptions{
+ PreloadCPE: true,
+ PreloadVulnerability: true,
+ PreloadBlob: true,
+ Vulnerabilities: v6.VulnerabilitySpecifiers{
+ {Name: "CVE-2023-0001"},
+ },
+ Limit: 0,
+ },
+ },
+ },
+ },
+ {
+ name: "only cpe spec provided",
+ config: AffectedPackagesOptions{
+ Package: v6.PackageSpecifiers{
+ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor1", Product: "product1"}},
+ },
+ CPE: v6.PackageSpecifiers{
+ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor2", Product: "product2"}},
+ },
+ },
+ expectedPkgCalls: []pkgCall{
+ {
+ pkg: &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: "a", Vendor: "vendor1", Product: "product1"}},
+ options: &v6.GetAffectedPackageOptions{
+ PreloadOS: true,
+ PreloadPackage: true,
+ PreloadVulnerability: true,
+ PreloadBlob: true,
+ Vulnerabilities: nil,
+ Limit: 0,
+ },
+ },
+ },
+ expectedCPECalls: []cpeCall{
+ {
+ cpe: &cpe.Attributes{Part: "a", Vendor: "vendor2", Product: "product2"},
+ options: &v6.GetAffectedCPEOptions{
+ PreloadCPE: true,
+ PreloadVulnerability: true,
+ PreloadBlob: true,
+ Vulnerabilities: nil,
+ Limit: 0,
+ },
+ },
+ },
+ expectedErr: nil,
+ },
+ {
+ name: "cpe + os spec provided",
+ config: AffectedPackagesOptions{
+ Package: v6.PackageSpecifiers{
+ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor1", Product: "product1"}},
+ },
+ CPE: v6.PackageSpecifiers{
+ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor2", Product: "product2"}},
+ },
+ OS: v6.OSSpecifiers{
+ {Name: "debian", MajorVersion: "10"}, // this prevents an agnostic CPE search
+ },
+ },
+ expectedPkgCalls: []pkgCall{
+ {
+ pkg: &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: "a", Vendor: "vendor1", Product: "product1"}},
+ options: &v6.GetAffectedPackageOptions{
+ PreloadOS: true,
+ PreloadPackage: true,
+ PreloadVulnerability: true,
+ PreloadBlob: true,
+ Vulnerabilities: nil,
+ OSs: v6.OSSpecifiers{
+ {Name: "debian", MajorVersion: "10"},
+ },
+ Limit: 0,
+ },
+ },
+ },
+ expectedCPECalls: nil,
+ expectedErr: nil,
+ },
+ {
+ name: "pkg spec provided",
+ config: AffectedPackagesOptions{
+ Package: v6.PackageSpecifiers{
+ {Name: "test-package", Ecosystem: "npm"},
+ },
+ },
+ expectedPkgCalls: []pkgCall{
+ {
+ pkg: &v6.PackageSpecifier{Name: "test-package", Ecosystem: "npm"},
+ options: &v6.GetAffectedPackageOptions{
+ PreloadOS: true,
+ PreloadPackage: true,
+ PreloadVulnerability: true,
+ PreloadBlob: true,
+ Vulnerabilities: nil,
+ Limit: 0,
+ },
+ },
+ },
+ expectedCPECalls: nil,
+ },
+
+ {
+ name: "pkg and os specs provided",
+ config: AffectedPackagesOptions{
+ Package: v6.PackageSpecifiers{
+ {Name: "test-package", Ecosystem: "npm"},
+ },
+ OS: v6.OSSpecifiers{
+ {Name: "debian", MajorVersion: "10"},
+ },
+ },
+ expectedPkgCalls: []pkgCall{
+ {
+ pkg: &v6.PackageSpecifier{Name: "test-package", Ecosystem: "npm"},
+ options: &v6.GetAffectedPackageOptions{
+ PreloadOS: true,
+ PreloadPackage: true,
+ PreloadVulnerability: true,
+ PreloadBlob: true,
+ OSs: v6.OSSpecifiers{
+ {Name: "debian", MajorVersion: "10"},
+ },
+ Limit: 0,
+ },
+ },
+ },
+ expectedCPECalls: nil,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ m := new(affectedMockReader)
+ defer m.AssertExpectations(t)
+
+ for _, expected := range tc.expectedPkgCalls {
+ m.On("GetAffectedPackages", expected.pkg, mock.MatchedBy(func(actual *v6.GetAffectedPackageOptions) bool {
+ return cmp.Equal(actual, expected.options)
+ })).Return([]v6.AffectedPackageHandle{}, nil).Once()
+ }
+
+ for _, expected := range tc.expectedCPECalls {
+ m.On("GetAffectedCPEs", expected.cpe, mock.MatchedBy(func(actual *v6.GetAffectedCPEOptions) bool {
+ return cmp.Equal(actual, expected.options)
+ })).Return([]v6.AffectedCPEHandle{}, nil).Once()
+ }
+
+ _, _, err := findAffectedPackages(m, tc.config)
+
+ if tc.expectedErr != nil {
+ require.ErrorIs(t, err, tc.expectedErr)
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
+}
+
+type affectedMockReader struct {
+ mock.Mock
+}
+
+func (m *affectedMockReader) GetAffectedPackages(pkgSpec *v6.PackageSpecifier, options *v6.GetAffectedPackageOptions) ([]v6.AffectedPackageHandle, error) {
+ args := m.Called(pkgSpec, options)
+ return args.Get(0).([]v6.AffectedPackageHandle), args.Error(1)
+}
+
+func (m *affectedMockReader) GetAffectedCPEs(cpeSpec *cpe.Attributes, options *v6.GetAffectedCPEOptions) ([]v6.AffectedCPEHandle, error) {
+ args := m.Called(cpeSpec, options)
+ return args.Get(0).([]v6.AffectedCPEHandle), args.Error(1)
+}
+
+func (m *affectedMockReader) GetKnownExploitedVulnerabilities(cve string) ([]v6.KnownExploitedVulnerabilityHandle, error) {
+ args := m.Called(cve)
+ return args.Get(0).([]v6.KnownExploitedVulnerabilityHandle), args.Error(1)
+}
+
+func (m *affectedMockReader) GetEpss(cve string) ([]v6.EpssHandle, error) {
+ args := m.Called(cve)
+ return args.Get(0).([]v6.EpssHandle), args.Error(1)
+}
+
+func ptr[T any](t T) *T {
+ return &t
+}
diff --git a/cmd/grype/cli/commands/internal/dbsearch/matches.go b/cmd/grype/cli/commands/internal/dbsearch/matches.go
new file mode 100644
index 00000000000..0e3aba8399f
--- /dev/null
+++ b/cmd/grype/cli/commands/internal/dbsearch/matches.go
@@ -0,0 +1,138 @@
+package dbsearch
+
+import (
+ "errors"
+ "fmt"
+ "sort"
+
+ "github.com/hashicorp/go-multierror"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+)
+
+// Matches is the JSON document for the `db search` command
+type Matches []Match
+
+// Match represents a pairing of a vulnerability advisory with the packages affected by the vulnerability.
+type Match struct {
+ // Vulnerability is the core advisory record for a single known vulnerability from a specific provider.
+ Vulnerability VulnerabilityInfo `json:"vulnerability"`
+
+ // AffectedPackages is the list of packages affected by the vulnerability.
+ AffectedPackages []AffectedPackageInfo `json:"packages"`
+}
+
+func (m Match) Flatten() []AffectedPackage {
+ var rows []AffectedPackage
+ for _, pkg := range m.AffectedPackages {
+ rows = append(rows, AffectedPackage{
+ Vulnerability: m.Vulnerability,
+ AffectedPackageInfo: pkg,
+ })
+ }
+ return rows
+}
+
+func (m Matches) Flatten() []AffectedPackage {
+ var rows []AffectedPackage
+ for _, r := range m {
+ rows = append(rows, r.Flatten()...)
+ }
+ return rows
+}
+
+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)
+
+ for i := range affectedPkgs {
+ pkg := affectedPkgs[i]
+ var detail v6.AffectedPackageBlob
+ if pkg.BlobValue != nil {
+ detail = *pkg.BlobValue
+ }
+ if pkg.Vulnerability == nil {
+ retErr = multierror.Append(retErr, fmt.Errorf("affected package record missing vulnerability: %+v", pkg))
+ continue
+ }
+ if _, ok := vulnsByID[pkg.Vulnerability.ID]; !ok {
+ vulnsByID[pkg.Vulnerability.ID] = *pkg.Vulnerability
+ decorationsByID[pkg.Vulnerability.ID] = pkg.vulnerabilityDecorations
+ }
+
+ aff := AffectedPackageInfo{
+ 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)
+ }
+
+ for _, ac := range affectedCPEs {
+ var detail v6.AffectedPackageBlob
+ if ac.BlobValue != nil {
+ detail = *ac.BlobValue
+ }
+ if ac.Vulnerability == nil {
+ retErr = multierror.Append(retErr, fmt.Errorf("affected CPE record missing vulnerability: %+v", ac))
+ continue
+ }
+
+ var c *CPE
+ if ac.CPE != nil {
+ cv := CPE(*ac.CPE)
+ c = &cv
+ }
+
+ if _, ok := vulnsByID[ac.Vulnerability.ID]; !ok {
+ vulnsByID[ac.Vulnerability.ID] = *ac.Vulnerability
+ decorationsByID[ac.Vulnerability.ID] = ac.vulnerabilityDecorations
+ }
+
+ aff := AffectedPackageInfo{
+ // tracking model information is not possible with CPE handles
+ 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)
+ }
+
+ for vulnID, vuln := range vulnsByID {
+ rows = append(rows, Match{
+ Vulnerability: newVulnerabilityInfo(vuln, decorationsByID[vulnID]),
+ AffectedPackages: affectedPkgsByVuln[vulnID],
+ })
+ }
+
+ sort.Slice(rows, func(i, j int) bool {
+ return rows[i].Vulnerability.ID < rows[j].Vulnerability.ID
+ })
+
+ return rows, retErr
+}
+
+func FindMatches(reader interface {
+ v6.AffectedPackageStoreReader
+ v6.AffectedCPEStoreReader
+ v6.VulnerabilityDecoratorStoreReader
+}, criteria AffectedPackagesOptions) (Matches, error) {
+ allAffectedPkgs, allAffectedCPEs, fetchErr := findAffectedPackages(reader, criteria)
+
+ if fetchErr != nil {
+ if !errors.Is(fetchErr, v6.ErrLimitReached) {
+ return nil, fetchErr
+ }
+ }
+
+ rows, presErr := newMatchesRows(allAffectedPkgs, allAffectedCPEs)
+ if presErr != nil {
+ return nil, presErr
+ }
+ return rows, fetchErr
+}
diff --git a/cmd/grype/cli/commands/internal/dbsearch/versions.go b/cmd/grype/cli/commands/internal/dbsearch/versions.go
new file mode 100644
index 00000000000..fd795136133
--- /dev/null
+++ b/cmd/grype/cli/commands/internal/dbsearch/versions.go
@@ -0,0 +1,18 @@
+package dbsearch
+
+const (
+ // MatchesSchemaVersion is the schema version for the `db search ` command
+ 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"
+
+ // VulnerabilitiesSchemaVersion
+ // 1.0.0 - Initial schema 🎉
+ // 1.0.1 - Add KEV and EPSS data to vulnerability
+)
diff --git a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go
new file mode 100644
index 00000000000..6b4c7622af0
--- /dev/null
+++ b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go
@@ -0,0 +1,270 @@
+package dbsearch
+
+import (
+ "errors"
+ "fmt"
+ "sort"
+ "time"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/internal/cvss"
+ "github.com/anchore/grype/internal/log"
+)
+
+// Vulnerabilities is the JSON document for the `db search vuln` command
+type Vulnerabilities []Vulnerability
+
+// Vulnerability represents the core advisory record for a single known vulnerability from a specific provider.
+type Vulnerability struct {
+ VulnerabilityInfo `json:",inline"`
+
+ // OperatingSystems is a list of operating systems affected by the vulnerability
+ OperatingSystems []OperatingSystem `json:"operating_systems"`
+
+ // AffectedPackages is the number of packages affected by the vulnerability
+ AffectedPackages int `json:"affected_packages"`
+}
+
+type VulnerabilityInfo struct {
+ // TODO: remove this when namespace is no longer used
+ Model v6.VulnerabilityHandle `json:"-"` // tracking package handle info is necessary for namespace lookup
+
+ v6.VulnerabilityBlob `json:",inline"`
+
+ // Provider is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider
+ // should be scoped to a specific vulnerability dataset, for instance, the "ubuntu" provider for all records from
+ // Canonicals' Ubuntu Security Notices (for all Ubuntu distro versions).
+ Provider string `json:"provider"`
+
+ // Status conveys the actionability of the current record (one of "active", "analyzing", "rejected", "disputed")
+ Status string `json:"status"`
+
+ // PublishedDate is the date the vulnerability record was first published
+ PublishedDate *time.Time `json:"published_date,omitempty"`
+
+ // ModifiedDate is the date the vulnerability record was last modified
+ ModifiedDate *time.Time `json:"modified_date,omitempty"`
+
+ // WithdrawnDate is the date the vulnerability record was withdrawn
+ WithdrawnDate *time.Time `json:"withdrawn_date,omitempty"`
+
+ // KnownExploited is a list of known exploited vulnerabilities from the CISA KEV dataset
+ KnownExploited []KnownExploited `json:"known_exploited,omitempty"`
+
+ // EPSS is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability
+ EPSS []EPSS `json:"epss,omitempty"`
+}
+
+// OperatingSystem represents specific release of an operating system.
+type OperatingSystem struct {
+ // Name is the operating system family name (e.g. "debian")
+ Name string `json:"name"`
+
+ // Version is the semver-ish or codename for the release of the operating system
+ Version string `json:"version"`
+}
+
+type KnownExploited struct {
+ CVE string `json:"cve"`
+ VendorProject string `json:"vendor_project,omitempty"`
+ Product string `json:"product,omitempty"`
+ DateAdded string `json:"date_added,omitempty"`
+ RequiredAction string `json:"required_action,omitempty"`
+ DueDate string `json:"due_date,omitempty"`
+ KnownRansomwareCampaignUse string `json:"known_ransomware_campaign_use"`
+ Notes string `json:"notes,omitempty"`
+ URLs []string `json:"urls,omitempty"`
+ CWEs []string `json:"cwes,omitempty"`
+}
+
+type EPSS struct {
+ CVE string `json:"cve"`
+ EPSS float64 `json:"epss"`
+ Percentile float64 `json:"percentile"`
+ 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
+ AffectedPackages int
+ vulnerabilityDecorations
+}
+
+type VulnerabilitiesOptions struct {
+ Vulnerability v6.VulnerabilitySpecifiers
+ RecordLimit int
+}
+
+func newVulnerabilityRows(vaps ...vulnerabilityAffectedPackageJoin) (rows []Vulnerability) {
+ for _, vap := range vaps {
+ rows = append(rows, Vulnerability{
+ VulnerabilityInfo: newVulnerabilityInfo(vap.Vulnerability, vap.vulnerabilityDecorations),
+ OperatingSystems: newOperatingSystems(vap.OperatingSystems),
+ AffectedPackages: vap.AffectedPackages,
+ })
+ }
+ return rows
+}
+
+func newVulnerabilityInfo(vuln v6.VulnerabilityHandle, vc vulnerabilityDecorations) VulnerabilityInfo {
+ var blob v6.VulnerabilityBlob
+ if vuln.BlobValue != nil {
+ blob = *vuln.BlobValue
+ }
+ patchCVSSMetrics(&blob)
+ return VulnerabilityInfo{
+ Model: vuln,
+ VulnerabilityBlob: blob,
+ Provider: vuln.Provider.ID,
+ Status: string(vuln.Status),
+ PublishedDate: vuln.PublishedDate,
+ ModifiedDate: vuln.ModifiedDate,
+ WithdrawnDate: vuln.WithdrawnDate,
+ KnownExploited: vc.KnownExploited,
+ EPSS: vc.EPSS,
+ }
+}
+
+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{
+ Name: o.Name,
+ Version: o.Version(),
+ })
+ }
+ return os
+}
+
+func FindVulnerabilities(reader interface { //nolint:funlen
+ v6.VulnerabilityStoreReader
+ v6.AffectedPackageStoreReader
+ v6.VulnerabilityDecoratorStoreReader
+}, config VulnerabilitiesOptions) ([]Vulnerability, error) {
+ log.WithFields("vulnSpecs", len(config.Vulnerability)).Debug("fetching vulnerabilities")
+
+ if config.RecordLimit == 0 {
+ log.Warn("no record limit set! For queries with large result sets this may result in performance issues")
+ }
+
+ var vulns []v6.VulnerabilityHandle
+ var limitReached bool
+ for _, vulnSpec := range config.Vulnerability {
+ vs, err := reader.GetVulnerabilities(&vulnSpec, &v6.GetVulnerabilityOptions{
+ Preload: true,
+ Limit: config.RecordLimit,
+ })
+ if err != nil {
+ if !errors.Is(err, v6.ErrLimitReached) {
+ return nil, fmt.Errorf("unable to get vulnerabilities: %w", err)
+ }
+ limitReached = true
+ break
+ }
+
+ vulns = append(vulns, vs...)
+ }
+
+ log.WithFields("vulns", len(vulns)).Debug("fetching affected packages")
+
+ // find all affected packages for this vulnerability, so we can gather os information
+ var pairs []vulnerabilityAffectedPackageJoin
+ for _, vuln := range vulns {
+ affected, fetchErr := reader.GetAffectedPackages(nil, &v6.GetAffectedPackageOptions{
+ PreloadOS: true,
+ Vulnerabilities: []v6.VulnerabilitySpecifier{
+ {
+ ID: vuln.ID,
+ },
+ },
+ Limit: config.RecordLimit,
+ })
+ if fetchErr != nil {
+ if !errors.Is(fetchErr, v6.ErrLimitReached) {
+ return nil, fmt.Errorf("unable to get affected packages: %w", fetchErr)
+ }
+ limitReached = true
+ }
+
+ distros := make(map[v6.ID]v6.OperatingSystem)
+ for _, a := range affected {
+ if a.OperatingSystem != nil {
+ if _, ok := distros[a.OperatingSystem.ID]; !ok {
+ distros[a.OperatingSystem.ID] = *a.OperatingSystem
+ }
+ }
+ }
+
+ var distrosSlice []v6.OperatingSystem
+ for _, d := range distros {
+ distrosSlice = append(distrosSlice, d)
+ }
+
+ sort.Slice(distrosSlice, func(i, j int) bool {
+ return distrosSlice[i].ID < distrosSlice[j].ID
+ })
+
+ pairs = append(pairs, vulnerabilityAffectedPackageJoin{
+ Vulnerability: vuln,
+ OperatingSystems: distrosSlice,
+ AffectedPackages: len(affected),
+ })
+
+ if errors.Is(fetchErr, v6.ErrLimitReached) {
+ break
+ }
+ }
+
+ for i := range pairs {
+ if err := decorateVulnerabilities(reader, &pairs[i]); err != nil {
+ return nil, fmt.Errorf("unable to decorate vulnerability: %w", err)
+ }
+ }
+
+ var err error
+ if limitReached {
+ err = v6.ErrLimitReached
+ }
+
+ return newVulnerabilityRows(pairs...), err
+}
diff --git a/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go
new file mode 100644
index 00000000000..0536611388f
--- /dev/null
+++ b/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go
@@ -0,0 +1,260 @@
+package dbsearch
+
+import (
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+)
+
+func TestNewVulnerabilityRows(t *testing.T) {
+ vap := vulnerabilityAffectedPackageJoin{
+ Vulnerability: v6.VulnerabilityHandle{
+ ID: 1,
+ Name: "CVE-1234-5678",
+ 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,
+ Provider: &v6.Provider{ID: "provider1"},
+ BlobValue: &v6.VulnerabilityBlob{Description: "Test description"},
+ },
+ OperatingSystems: []v6.OperatingSystem{
+ {Name: "Linux", MajorVersion: "5", MinorVersion: "10"},
+ },
+ AffectedPackages: 5,
+ vulnerabilityDecorations: vulnerabilityDecorations{
+ KnownExploited: []KnownExploited{
+ {
+ CVE: "CVE-1234-5678",
+ VendorProject: "LinuxFoundation",
+ Product: "Linux",
+ DateAdded: "2025-02-02",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-02",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-1234"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-1234-5678",
+ EPSS: 0.893,
+ Percentile: 0.99,
+ Date: "2025-02-24",
+ },
+ },
+ },
+ }
+
+ rows := newVulnerabilityRows(vap)
+ 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,
+ KnownExploited: []KnownExploited{
+ {
+ CVE: "CVE-1234-5678",
+ VendorProject: "LinuxFoundation",
+ Product: "Linux",
+ DateAdded: "2025-02-02",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-02",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-1234"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-1234-5678",
+ EPSS: 0.893,
+ Percentile: 0.99,
+ Date: "2025-02-24",
+ },
+ },
+ },
+ OperatingSystems: []OperatingSystem{
+ {Name: "Linux", Version: "5.10"},
+ },
+ AffectedPackages: 5,
+ },
+ }
+
+ if diff := cmp.Diff(expected, rows, cmpOpts()...); diff != "" {
+ t.Errorf("unexpected rows (-want +got):\n%s", diff)
+ }
+}
+
+func TestVulnerabilities(t *testing.T) {
+ mockReader := new(mockVulnReader)
+ vulnSpecs := v6.VulnerabilitySpecifiers{
+ {Name: "CVE-1234-5678"},
+ }
+
+ mockReader.On("GetVulnerabilities", mock.Anything, mock.Anything).Return([]v6.VulnerabilityHandle{
+ {
+ ID: 1,
+ Name: "CVE-1234-5678",
+ 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)),
+ Provider: &v6.Provider{ID: "provider1"},
+ 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)
+
+ mockReader.On("GetAffectedPackages", mock.Anything, mock.Anything).Return([]v6.AffectedPackageHandle{
+ {
+ OperatingSystem: &v6.OperatingSystem{Name: "Linux", MajorVersion: "5", MinorVersion: "10"},
+ },
+ }, nil)
+
+ mockReader.On("GetKnownExploitedVulnerabilities", "CVE-1234-5678").Return([]v6.KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-1234-5678",
+ BlobValue: &v6.KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-1234-5678",
+ VendorProject: "LinuxFoundation",
+ Product: "Linux",
+ DateAdded: ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)),
+ RequiredAction: "Yes",
+ DueDate: ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)),
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-1234"},
+ },
+ },
+ }, nil)
+
+ mockReader.On("GetEpss", "CVE-1234-5678").Return([]v6.EpssHandle{
+ {
+ Cve: "CVE-1234-5678",
+ Epss: 0.893,
+ Percentile: 0.99,
+ Date: time.Date(2025, 2, 24, 0, 0, 0, 0, time.UTC),
+ },
+ }, nil)
+
+ results, err := FindVulnerabilities(mockReader, VulnerabilitiesOptions{Vulnerability: vulnSpecs})
+ require.NoError(t, err)
+
+ expected := []Vulnerability{
+ {
+ VulnerabilityInfo: VulnerabilityInfo{
+ 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",
+ VendorProject: "LinuxFoundation",
+ Product: "Linux",
+ DateAdded: "2025-02-02",
+ RequiredAction: "Yes",
+ DueDate: "2025-02-02",
+ KnownRansomwareCampaignUse: "Known",
+ Notes: "note!",
+ URLs: []string{"https://example.com"},
+ CWEs: []string{"CWE-1234"},
+ },
+ },
+ EPSS: []EPSS{
+ {
+ CVE: "CVE-1234-5678",
+ EPSS: 0.893,
+ Percentile: 0.99,
+ Date: "2025-02-24",
+ },
+ },
+ },
+ OperatingSystems: []OperatingSystem{
+ {Name: "Linux", Version: "5.10"},
+ },
+ AffectedPackages: 1,
+ },
+ }
+
+ if diff := cmp.Diff(expected, results, cmpOpts()...); diff != "" {
+ t.Errorf("unexpected results (-want +got):\n%s", diff)
+ }
+}
+
+type mockVulnReader struct {
+ mock.Mock
+}
+
+func (m *mockVulnReader) GetVulnerabilities(vuln *v6.VulnerabilitySpecifier, config *v6.GetVulnerabilityOptions) ([]v6.VulnerabilityHandle, error) {
+ args := m.Called(vuln, config)
+ return args.Get(0).([]v6.VulnerabilityHandle), args.Error(1)
+}
+
+func (m *mockVulnReader) GetAffectedPackages(pkg *v6.PackageSpecifier, config *v6.GetAffectedPackageOptions) ([]v6.AffectedPackageHandle, error) {
+ args := m.Called(pkg, config)
+ return args.Get(0).([]v6.AffectedPackageHandle), args.Error(1)
+}
+
+func (m *mockVulnReader) GetKnownExploitedVulnerabilities(cve string) ([]v6.KnownExploitedVulnerabilityHandle, error) {
+ args := m.Called(cve)
+ return args.Get(0).([]v6.KnownExploitedVulnerabilityHandle), args.Error(1)
+}
+
+func (m *mockVulnReader) GetEpss(cve string) ([]v6.EpssHandle, error) {
+ args := m.Called(cve)
+ return args.Get(0).([]v6.EpssHandle), args.Error(1)
+}
+
+func cmpOpts() []cmp.Option {
+ return []cmp.Option{
+ cmpopts.IgnoreFields(AffectedPackageInfo{}, "Model"),
+ cmpopts.IgnoreFields(VulnerabilityInfo{}, "Model"),
+ }
+}
diff --git a/cmd/grype/cli/commands/internal/dbsearch/vulnerability_decorations.go b/cmd/grype/cli/commands/internal/dbsearch/vulnerability_decorations.go
new file mode 100644
index 00000000000..be443c64245
--- /dev/null
+++ b/cmd/grype/cli/commands/internal/dbsearch/vulnerability_decorations.go
@@ -0,0 +1,145 @@
+package dbsearch
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/go-multierror"
+ "github.com/scylladb/go-set/strset"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+)
+
+type canonicalVulnerability interface {
+ getCVEs() []string
+ decorate(kevs []KnownExploited, epss []EPSS)
+}
+
+// vulnerabilityDecorations are separate model elements (not from VulnerabilityHandle) that is fetched on
+// the provider.GetMetadata() path instead of the provider.GetVulnerabilities() path. I hope for these two paths
+// to be merged into the same path come grype 1.0, in which case these elements would already be on the
+// store get methods when crafting a vulnerability.
+type vulnerabilityDecorations struct {
+ KnownExploited []KnownExploited `json:"knownExploited,omitempty"`
+ EPSS []EPSS `json:"epss,omitempty"`
+}
+
+func decorateVulnerabilities(reader v6.VulnerabilityDecoratorStoreReader, cvs ...canonicalVulnerability) error {
+ for _, cv := range cvs {
+ cves := cv.getCVEs()
+ if len(cves) == 0 {
+ continue
+ }
+
+ knownExploited, err := fetchKnownExploited(reader, cves)
+ if err != nil {
+ return fmt.Errorf("unable to get known exploited vulnerabilities: %w", err)
+ }
+
+ epss, err := fetchEpss(reader, cves)
+ if err != nil {
+ return fmt.Errorf("unable to get EPSS scores: %w", err)
+ }
+
+ cv.decorate(knownExploited, epss)
+ }
+ return nil
+}
+
+func (afj *vulnerabilityAffectedPackageJoin) getCVEs() []string {
+ if afj == nil {
+ return nil
+ }
+ return getCVEs(&afj.Vulnerability)
+}
+
+func getCVEs(v *v6.VulnerabilityHandle) []string {
+ var cves []string
+ set := strset.New()
+
+ addCVE := func(id string) {
+ lower := strings.ToLower(id)
+ if strings.HasPrefix(lower, "cve-") {
+ if !set.Has(lower) {
+ cves = append(cves, id)
+ set.Add(lower)
+ }
+ }
+ }
+
+ if v == nil {
+ return cves
+ }
+
+ addCVE(v.Name)
+
+ if v.BlobValue == nil {
+ return cves
+ }
+
+ addCVE(v.BlobValue.ID)
+
+ for _, alias := range v.BlobValue.Aliases {
+ addCVE(alias)
+ }
+
+ return cves
+}
+
+func (vd *vulnerabilityDecorations) decorate(kevs []KnownExploited, epss []EPSS) {
+ if vd == nil {
+ return
+ }
+
+ vd.KnownExploited = kevs
+ vd.EPSS = epss
+}
+
+func fetchKnownExploited(reader v6.VulnerabilityDecoratorStoreReader, cves []string) ([]KnownExploited, error) {
+ var out []KnownExploited
+ var errs error
+ for _, cve := range cves {
+ kevs, err := reader.GetKnownExploitedVulnerabilities(cve)
+ if err != nil {
+ errs = multierror.Append(errs, err)
+ continue
+ }
+ for _, kev := range kevs {
+ out = append(out, KnownExploited{
+ CVE: kev.Cve,
+ VendorProject: kev.BlobValue.VendorProject,
+ Product: kev.BlobValue.Product,
+ DateAdded: kev.BlobValue.DateAdded.Format(time.DateOnly),
+ RequiredAction: kev.BlobValue.RequiredAction,
+ DueDate: kev.BlobValue.DueDate.Format(time.DateOnly),
+ KnownRansomwareCampaignUse: kev.BlobValue.KnownRansomwareCampaignUse,
+ Notes: kev.BlobValue.Notes,
+ URLs: kev.BlobValue.URLs,
+ CWEs: kev.BlobValue.CWEs,
+ })
+ }
+ }
+ return out, errs
+}
+
+func fetchEpss(reader v6.VulnerabilityDecoratorStoreReader, cves []string) ([]EPSS, error) {
+ var out []EPSS
+ var errs error
+ for _, cve := range cves {
+ entries, err := reader.GetEpss(cve)
+ if err != nil {
+ errs = multierror.Append(errs, err)
+ continue
+ }
+ for _, entry := range entries {
+ out = append(out, EPSS{
+ CVE: entry.Cve,
+ EPSS: entry.Epss,
+ Percentile: entry.Percentile,
+ Date: entry.Date.Format(time.DateOnly),
+ })
+ }
+ }
+ return out, errs
+}
diff --git a/cmd/grype/cli/commands/internal/jsonschema/main.go b/cmd/grype/cli/commands/internal/jsonschema/main.go
new file mode 100644
index 00000000000..c8467536c73
--- /dev/null
+++ b/cmd/grype/cli/commands/internal/jsonschema/main.go
@@ -0,0 +1,263 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "go/ast"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "reflect"
+ "strings"
+
+ "github.com/invopop/jsonschema"
+ "golang.org/x/tools/go/packages"
+
+ "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch"
+)
+
+func main() {
+ pkgPatterns := []string{"../dbsearch", "../../../../../../grype/db/v6"}
+
+ comments := parseCommentsFromPackages(pkgPatterns)
+ fmt.Printf("Extracted field comments from %d structs\n", len(comments))
+
+ compose(dbsearch.Matches{}, "db-search", dbsearch.MatchesSchemaVersion, comments)
+ compose(dbsearch.Vulnerabilities{}, "db-search-vuln", dbsearch.VulnerabilitiesSchemaVersion, comments)
+}
+
+func compose(document any, component, version string, comments map[string]map[string]string) {
+ write(encode(build(document, component, version, comments)), component, version)
+}
+
+func write(schema []byte, component, version string) {
+ parent := filepath.Join(repoRoot(), "schema", "grype", component, "json")
+ schemaPath := filepath.Join(parent, fmt.Sprintf("schema-%s.json", version))
+ latestSchemaPath := filepath.Join(parent, "schema-latest.json")
+
+ if _, err := os.Stat(schemaPath); !os.IsNotExist(err) {
+ // check if the schema is the same...
+ existingFh, err := os.Open(schemaPath)
+ if err != nil {
+ panic(err)
+ }
+
+ existingSchemaBytes, err := io.ReadAll(existingFh)
+ if err != nil {
+ panic(err)
+ }
+
+ if bytes.Equal(existingSchemaBytes, schema) {
+ // the generated schema is the same, bail with no error :)
+ fmt.Printf("No change to the existing %q schema!\n", component)
+ return
+ }
+
+ // the generated schema is different, bail with error :(
+ fmt.Printf("Cowardly refusing to overwrite existing %q schema (%s)!\nSee the README.md for how to increment\n", component, schemaPath)
+ os.Exit(1)
+ }
+
+ fh, err := os.Create(schemaPath)
+ if err != nil {
+ panic(err)
+ }
+ defer fh.Close()
+
+ _, err = fh.Write(schema)
+ if err != nil {
+ panic(err)
+ }
+
+ latestFile, err := os.Create(latestSchemaPath)
+ if err != nil {
+ panic(err)
+ }
+ defer latestFile.Close()
+
+ _, err = latestFile.Write(schema)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("Wrote new %q schema to %q\n", component, schemaPath)
+}
+
+func encode(schema *jsonschema.Schema) []byte {
+ newSchemaBuffer := new(bytes.Buffer)
+ enc := json.NewEncoder(newSchemaBuffer)
+ // prevent > and < from being escaped in the payload
+ enc.SetEscapeHTML(false)
+ enc.SetIndent("", " ")
+ err := enc.Encode(&schema)
+ if err != nil {
+ panic(err)
+ }
+
+ return newSchemaBuffer.Bytes()
+}
+
+func build(document any, component, version string, comments map[string]map[string]string) *jsonschema.Schema {
+ reflector := &jsonschema.Reflector{
+ BaseSchemaID: schemaID(component, version),
+ AllowAdditionalProperties: true,
+ Namer: func(r reflect.Type) string {
+ return strings.TrimPrefix(r.Name(), "JSON")
+ },
+ }
+
+ documentSchema := reflector.ReflectFromType(reflect.TypeOf(document))
+
+ for structName, fields := range comments {
+ if structSchema, exists := documentSchema.Definitions[structName]; exists {
+ if structSchema.Definitions == nil {
+ structSchema.Definitions = make(map[string]*jsonschema.Schema)
+ }
+ for fieldName, comment := range fields {
+ if fieldName == "" {
+ // struct-level comment
+ structSchema.Description = comment
+ continue
+ }
+ // field level comment
+ if comment == "" {
+ continue
+ }
+ if _, exists := structSchema.Properties.Get(fieldName); exists {
+ fieldSchema, exists := structSchema.Definitions[fieldName]
+ if exists {
+ fieldSchema.Description = comment
+ } else {
+ fieldSchema = &jsonschema.Schema{
+ Description: comment,
+ }
+ }
+ structSchema.Definitions[fieldName] = fieldSchema
+ }
+ }
+ documentSchema.Definitions[structName] = structSchema
+ }
+ }
+
+ return documentSchema
+}
+
+// parseCommentsFromPackages scans multiple packages and collects field comments for structs.
+func parseCommentsFromPackages(pkgPatterns []string) map[string]map[string]string {
+ commentMap := make(map[string]map[string]string)
+
+ cfg := &packages.Config{
+ Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedDeps | packages.NeedImports,
+ }
+ pkgs, err := packages.Load(cfg, pkgPatterns...)
+ if err != nil {
+ panic(fmt.Errorf("failed to load packages: %w", err))
+ }
+
+ for _, pkg := range pkgs {
+ for _, file := range pkg.Syntax {
+ fileComments := parseFileComments(file)
+ for structName, fields := range fileComments {
+ if _, exists := commentMap[structName]; !exists {
+ commentMap[structName] = fields
+ }
+ }
+ }
+ }
+ return commentMap
+}
+
+// parseFileComments extracts comments for structs and their fields in a single file.
+func parseFileComments(node *ast.File) map[string]map[string]string {
+ commentMap := make(map[string]map[string]string)
+
+ ast.Inspect(node, func(n ast.Node) bool {
+ ts, ok := n.(*ast.TypeSpec)
+ if !ok {
+ return true
+ }
+ st, ok := ts.Type.(*ast.StructType)
+ if !ok {
+ return true
+ }
+
+ structName := ts.Name.Name
+ fieldComments := make(map[string]string)
+
+ // extract struct-level comment
+ if ts.Doc != nil {
+ structComment := strings.TrimSpace(ts.Doc.Text())
+ if !strings.Contains(structComment, "TODO:") {
+ fieldComments[""] = cleanComment(structComment)
+ }
+ }
+
+ // extract field-level comments
+ for _, field := range st.Fields.List {
+ if len(field.Names) == 0 {
+ continue
+ }
+ fieldName := field.Names[0].Name
+ jsonTag := getJSONTag(field)
+
+ if field.Doc != nil {
+ comment := strings.TrimSpace(field.Doc.Text())
+ if strings.Contains(comment, "TODO:") {
+ continue
+ }
+ if jsonTag != "" {
+ fieldComments[jsonTag] = cleanComment(comment)
+ } else {
+ fieldComments[fieldName] = cleanComment(comment)
+ }
+ }
+ }
+
+ if len(fieldComments) > 0 {
+ commentMap[structName] = fieldComments
+ }
+ return true
+ })
+
+ return commentMap
+}
+
+func cleanComment(comment string) string {
+ // remove the first word, since that is the field name (if following go-doc patterns)
+ split := strings.SplitN(comment, " ", 2)
+ if len(split) > 1 {
+ comment = split[1]
+ }
+
+ return strings.TrimSpace(strings.ReplaceAll(comment, "\"", "'"))
+}
+
+func getJSONTag(field *ast.Field) string {
+ if field.Tag != nil {
+ tagValue := strings.Trim(field.Tag.Value, "`")
+ structTag := reflect.StructTag(tagValue)
+ if jsonTag, ok := structTag.Lookup("json"); ok {
+ jsonParts := strings.Split(jsonTag, ",")
+ return strings.TrimSpace(jsonParts[0])
+ }
+ }
+ return ""
+}
+
+func schemaID(component, version string) jsonschema.ID {
+ return jsonschema.ID(fmt.Sprintf("anchore.io/schema/grype/%s/json/%s", component, version))
+}
+
+func repoRoot() string {
+ root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
+ if err != nil {
+ panic(fmt.Errorf("unable to find repo root dir: %+v", err))
+ }
+ absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))
+ if err != nil {
+ panic(fmt.Errorf("unable to get abs path to repo root: %w", err))
+ }
+ return absRepoRoot
+}
diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go
new file mode 100644
index 00000000000..937797e04c5
--- /dev/null
+++ b/cmd/grype/cli/commands/root.go
@@ -0,0 +1,425 @@
+package commands
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/spf13/cobra"
+ "github.com/wagoodman/go-partybus"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/options"
+ "github.com/anchore/grype/grype"
+ "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"
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher"
+ "github.com/anchore/grype/grype/matcher/dotnet"
+ "github.com/anchore/grype/grype/matcher/golang"
+ "github.com/anchore/grype/grype/matcher/java"
+ "github.com/anchore/grype/grype/matcher/javascript"
+ "github.com/anchore/grype/grype/matcher/python"
+ "github.com/anchore/grype/grype/matcher/ruby"
+ "github.com/anchore/grype/grype/matcher/stock"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/presenter/models"
+ "github.com/anchore/grype/grype/vex"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal"
+ "github.com/anchore/grype/internal/bus"
+ "github.com/anchore/grype/internal/format"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/grype/internal/stringutil"
+ "github.com/anchore/syft/syft"
+ "github.com/anchore/syft/syft/cataloging"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/sbom"
+)
+
+func Root(app clio.Application) *cobra.Command {
+ opts := options.DefaultGrype(app.ID())
+
+ return app.SetupRootCommand(&cobra.Command{
+ Use: fmt.Sprintf("%s [IMAGE]", app.ID().Name),
+ Short: "A vulnerability scanner for container images, filesystems, and SBOMs",
+ Long: stringutil.Tprintf(`A vulnerability scanner for container images, filesystems, and SBOMs.
+
+Supports the following image sources:
+ {{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
+ {{.appName}} path/to/yourproject a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
+
+You can also explicitly specify the scheme to use:
+ {{.appName}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
+ {{.appName}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
+ {{.appName}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
+ {{.appName}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Podman or otherwise)
+ {{.appName}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
+ {{.appName}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
+ {{.appName}} dir:path/to/yourproject read directly from a path on disk (any directory)
+ {{.appName}} file:path/to/yourfile read directly from a file on disk
+ {{.appName}} sbom:path/to/syft.json read Syft JSON from path on disk
+ {{.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}}
+
+`, map[string]interface{}{
+ "appName": app.ID().Name,
+ }),
+ Args: validateRootArgs,
+ SilenceUsage: true,
+ SilenceErrors: true,
+ RunE: func(_ *cobra.Command, args []string) error {
+ userInput := ""
+ if len(args) > 0 {
+ userInput = args[0]
+ }
+ return runGrype(app, opts, userInput)
+ },
+ ValidArgsFunction: dockerImageValidArgsFunction,
+ }, opts)
+}
+
+var ignoreNonFixedMatches = []match.IgnoreRule{
+ {FixState: string(vulnerability.FixStateNotFixed)},
+ {FixState: string(vulnerability.FixStateWontFix)},
+ {FixState: string(vulnerability.FixStateUnknown)},
+}
+
+var ignoreFixedMatches = []match.IgnoreRule{
+ {FixState: string(vulnerability.FixStateFixed)},
+}
+
+var ignoreVEXFixedNotAffected = []match.IgnoreRule{
+ {VexStatus: string(vex.StatusNotAffected)},
+ {VexStatus: string(vex.StatusFixed)},
+}
+
+var ignoreLinuxKernelHeaders = []match.IgnoreRule{
+ {Package: match.IgnoreRulePackage{Name: "kernel-headers", UpstreamName: "kernel", Type: string(syftPkg.RpmPkg)}, MatchType: match.ExactIndirectMatch},
+ {Package: match.IgnoreRulePackage{Name: "linux(-.*)?-headers-.*", UpstreamName: "linux.*", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch},
+ {Package: match.IgnoreRulePackage{Name: "linux-libc-dev", UpstreamName: "linux", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch},
+}
+
+//nolint:funlen
+func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs error) {
+ writer, err := format.MakeScanResultWriter(opts.Outputs, opts.File, format.PresentationConfig{
+ TemplateFilePath: opts.OutputTemplateFile,
+ ShowSuppressed: opts.ShowSuppressed,
+ Pretty: opts.Pretty,
+ })
+ if err != nil {
+ return err
+ }
+
+ var vp vulnerability.Provider
+ var status *vulnerability.ProviderStatus
+ var packages []pkg.Package
+ var s *sbom.SBOM
+ var pkgContext pkg.Context
+
+ if opts.OnlyFixed {
+ opts.Ignore = append(opts.Ignore, ignoreNonFixedMatches...)
+ }
+
+ if opts.OnlyNotFixed {
+ opts.Ignore = append(opts.Ignore, ignoreFixedMatches...)
+ }
+
+ if !opts.MatchUpstreamKernelHeaders {
+ opts.Ignore = append(opts.Ignore, ignoreLinuxKernelHeaders...)
+ }
+
+ for _, ignoreState := range stringutil.SplitCommaSeparatedString(opts.IgnoreStates) {
+ switch vulnerability.FixState(ignoreState) {
+ case vulnerability.FixStateUnknown, vulnerability.FixStateFixed, vulnerability.FixStateNotFixed, vulnerability.FixStateWontFix:
+ opts.Ignore = append(opts.Ignore, match.IgnoreRule{FixState: ignoreState})
+ default:
+ return fmt.Errorf("unknown fix state %s was supplied for --ignore-states", ignoreState)
+ }
+ }
+
+ err = parallel(
+ func() error {
+ checkForAppUpdate(app.ID(), opts)
+ 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
+ // grype uses the SBOM in combination with syft formatters to produce cycloneDX
+ // with vulnerability information appended
+ packages, pkgContext, s, err = pkg.Provide(userInput, getProviderConfig(opts))
+ if err != nil {
+ return fmt.Errorf("failed to catalog: %w", err)
+ }
+ return nil
+ },
+ )
+
+ if err != nil {
+ return err
+ }
+
+ defer log.CloseAndLogError(vp, status.Path)
+
+ if err = applyVexRules(opts); err != nil {
+ return fmt.Errorf("applying vex rules: %w", err)
+ }
+
+ startTime := time.Now()
+ applyDistroHint(packages, &pkgContext, opts)
+
+ vulnMatcher := grype.VulnerabilityMatcher{
+ VulnerabilityProvider: vp,
+ IgnoreRules: opts.Ignore,
+ NormalizeByCVE: opts.ByCVE,
+ FailSeverity: opts.FailOnSeverity(),
+ Matchers: getMatchers(opts),
+ VexProcessor: vex.NewProcessor(vex.ProcessorOptions{
+ Documents: opts.VexDocuments,
+ IgnoreRules: opts.Ignore,
+ }),
+ }
+
+ remainingMatches, ignoredMatches, err := vulnMatcher.FindMatches(packages, pkgContext)
+ if err != nil {
+ if !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {
+ return err
+ }
+ errs = appendErrors(errs, err)
+ }
+
+ 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,
+ 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)
+
+ split := strings.Split(opts.Distro, ":")
+ d := split[0]
+ v := ""
+ if len(split) > 1 {
+ v = split[1]
+ }
+ 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")
+ }
+ }
+
+ hasOSPackageWithoutDistro := false
+ for _, p := range pkgs {
+ switch p.Type {
+ case syftPkg.AlpmPkg, syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.KbPkg:
+ if p.Distro == nil {
+ hasOSPackageWithoutDistro = true
+ }
+ }
+ }
+
+ 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 :")
+ }
+}
+
+func checkForAppUpdate(id clio.Identification, opts *options.Grype) {
+ if !opts.CheckForAppUpdate {
+ return
+ }
+
+ isAvailable, newVersion, err := isUpdateAvailable(id)
+ if err != nil {
+ log.Errorf(err.Error())
+ }
+ if isAvailable {
+ log.Infof("new version of %s is available: %s (currently running: %s)", id.Name, newVersion, id.Version)
+
+ bus.Publish(partybus.Event{
+ Type: event.CLIAppUpdateAvailable,
+ Value: parsers.UpdateCheck{
+ New: newVersion,
+ Current: id.Version,
+ },
+ })
+ } else {
+ log.Debugf("no new %s application update available", id.Name)
+ }
+}
+
+func getMatchers(opts *options.Grype) []match.Matcher {
+ return matcher.NewDefaultMatchers(
+ matcher.Config{
+ Java: java.MatcherConfig{
+ ExternalSearchConfig: opts.ExternalSources.ToJavaMatcherConfig(),
+ UseCPEs: opts.Match.Java.UseCPEs,
+ },
+ Ruby: ruby.MatcherConfig(opts.Match.Ruby),
+ Python: python.MatcherConfig(opts.Match.Python),
+ Dotnet: dotnet.MatcherConfig(opts.Match.Dotnet),
+ Javascript: javascript.MatcherConfig(opts.Match.Javascript),
+ Golang: golang.MatcherConfig{
+ UseCPEs: opts.Match.Golang.UseCPEs,
+ AlwaysUseCPEForStdlib: opts.Match.Golang.AlwaysUseCPEForStdlib,
+ AllowMainModulePseudoVersionComparison: opts.Match.Golang.AllowMainModulePseudoVersionComparison,
+ },
+ Stock: stock.MatcherConfig(opts.Match.Stock),
+ },
+ )
+}
+
+func getProviderConfig(opts *options.Grype) pkg.ProviderConfig {
+ cfg := syft.DefaultCreateSBOMConfig()
+ cfg.Packages.JavaArchive.IncludeIndexedArchives = opts.Search.IncludeIndexedArchives
+ cfg.Packages.JavaArchive.IncludeUnindexedArchives = opts.Search.IncludeUnindexedArchives
+
+ // when we run into a package with missing information like version, then this is not useful in the context
+ // of vulnerability matching. Though there will be downstream processing to handle this case, we can still
+ // save us the effort of ever attempting to match with these packages as early as possible.
+ cfg.Compliance.MissingVersion = cataloging.ComplianceActionDrop
+
+ return pkg.ProviderConfig{
+ SyftProviderConfig: pkg.SyftProviderConfig{
+ RegistryOptions: opts.Registry.ToOptions(),
+ Exclusions: opts.Exclusions,
+ SBOMOptions: cfg,
+ Platform: opts.Platform,
+ Name: opts.Name,
+ DefaultImagePullSource: opts.DefaultImagePullSource,
+ },
+ SynthesisConfig: pkg.SynthesisConfig{
+ GenerateMissingCPEs: opts.GenerateMissingCPEs,
+ },
+ }
+}
+
+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") {
+ bus.Notify("Database checksum invalid, run `grype db delete` to remove it and `grype db update` to update.")
+ }
+ if strings.Contains(loadErr.Error(), "import.json") {
+ bus.Notify("Unable to find database import metadata, run `grype db delete` to remove the existing database and `grype db update` to update.")
+ }
+ return fmt.Errorf("failed to load vulnerability db: %w", loadErr)
+ }
+ if status == nil {
+ return fmt.Errorf("unable to determine the status of the vulnerability db")
+ }
+ if status.Error != nil {
+ return fmt.Errorf("db could not be loaded: %w", status.Error)
+ }
+ return nil
+}
+
+func validateRootArgs(cmd *cobra.Command, args []string) error {
+ isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect()
+ if err != nil {
+ log.Warnf("unable to determine if there is piped input: %+v", err)
+ isStdinPipeOrRedirect = false
+ }
+
+ if len(args) == 0 && !isStdinPipeOrRedirect {
+ // in the case that no arguments are given and there is no piped input we want to show the help text and return with a non-0 return code.
+ if err := cmd.Help(); err != nil {
+ return fmt.Errorf("unable to display help: %w", err)
+ }
+ return fmt.Errorf("an image/directory argument is required")
+ }
+
+ // in the case that a single empty string argument ("") is given and there is no piped input we want to show the help text and return with a non-0 return code.
+ if len(args) != 0 && args[0] == "" && !isStdinPipeOrRedirect {
+ if err := cmd.Help(); err != nil {
+ return fmt.Errorf("unable to display help: %w", err)
+ }
+ return fmt.Errorf("an image/directory argument is required")
+ }
+
+ return cobra.MaximumNArgs(1)(cmd, args)
+}
+
+func applyVexRules(opts *options.Grype) error {
+ if len(opts.Ignore) == 0 && len(opts.VexDocuments) > 0 {
+ opts.Ignore = append(opts.Ignore, ignoreVEXFixedNotAffected...)
+ }
+
+ for _, vexStatus := range opts.VexAdd {
+ switch vexStatus {
+ case string(vex.StatusAffected):
+ opts.Ignore = append(
+ opts.Ignore, match.IgnoreRule{VexStatus: string(vex.StatusAffected)},
+ )
+ case string(vex.StatusUnderInvestigation):
+ opts.Ignore = append(
+ opts.Ignore, match.IgnoreRule{VexStatus: string(vex.StatusUnderInvestigation)},
+ )
+ default:
+ return fmt.Errorf("invalid VEX status in vex-add setting: %s", vexStatus)
+ }
+ }
+
+ return nil
+}
diff --git a/cmd/grype/cli/commands/root_test.go b/cmd/grype/cli/commands/root_test.go
new file mode 100644
index 00000000000..e50d3408908
--- /dev/null
+++ b/cmd/grype/cli/commands/root_test.go
@@ -0,0 +1,88 @@
+package commands
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli/options"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/stereoscope/pkg/image"
+ "github.com/anchore/syft/syft"
+ "github.com/anchore/syft/syft/cataloging"
+ "github.com/anchore/syft/syft/pkg/cataloger/binary"
+)
+
+func Test_applyDistroHint(t *testing.T) {
+ ctx := pkg.Context{}
+ cfg := options.Grype{}
+
+ applyDistroHint([]pkg.Package{}, &ctx, &cfg)
+ assert.Nil(t, ctx.Distro)
+
+ // works when distro is nil
+ cfg.Distro = "alpine:3.10"
+ applyDistroHint([]pkg.Package{}, &ctx, &cfg)
+ assert.NotNil(t, ctx.Distro)
+
+ assert.Equal(t, "alpine", ctx.Distro.Name())
+ assert.Equal(t, "3.10", ctx.Distro.Version)
+
+ // does override an existing distro
+ 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, "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, "24.04", ctx.Distro.Version)
+}
+
+func Test_getProviderConfig(t *testing.T) {
+ tests := []struct {
+ name string
+ opts *options.Grype
+ want pkg.ProviderConfig
+ }{
+ {
+ name: "syft default api options are used",
+ opts: options.DefaultGrype(clio.Identification{
+ Name: "test",
+ Version: "1.0",
+ }),
+ want: pkg.ProviderConfig{
+ SyftProviderConfig: pkg.SyftProviderConfig{
+ SBOMOptions: func() *syft.CreateSBOMConfig {
+ cfg := syft.DefaultCreateSBOMConfig()
+ cfg.Compliance.MissingVersion = cataloging.ComplianceActionDrop
+ return cfg
+ }(),
+ RegistryOptions: &image.RegistryOptions{
+ Credentials: []image.RegistryCredentials{},
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ opts := cmp.Options{
+ cmpopts.IgnoreFields(binary.Classifier{}, "EvidenceMatcher"),
+ cmpopts.IgnoreUnexported(syft.CreateSBOMConfig{}),
+ }
+ if d := cmp.Diff(tt.want, getProviderConfig(tt.opts), opts...); d != "" {
+ t.Errorf("getProviderConfig() mismatch (-want +got):\n%s", d)
+ }
+ })
+ }
+}
diff --git a/cmd/grype/cli/commands/test-fixtures/provider-metadata.json b/cmd/grype/cli/commands/test-fixtures/provider-metadata.json
new file mode 100644
index 00000000000..015f6914e41
--- /dev/null
+++ b/cmd/grype/cli/commands/test-fixtures/provider-metadata.json
@@ -0,0 +1,12 @@
+{
+ "providers": [
+ {
+ "name": "provider1",
+ "lastSuccessfulRun": "2024-10-16T01:33:16.844201Z"
+ },
+ {
+ "name": "provider2",
+ "lastSuccessfulRun": "2024-10-16T01:32:43.516596Z"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/internal/version/update.go b/cmd/grype/cli/commands/update.go
similarity index 67%
rename from internal/version/update.go
rename to cmd/grype/cli/commands/update.go
index 03c003f2cb0..cf95279ae8c 100644
--- a/internal/version/update.go
+++ b/cmd/grype/cli/commands/update.go
@@ -1,4 +1,4 @@
-package version
+package commands
import (
"fmt"
@@ -6,8 +6,9 @@ import (
"net/http"
"strings"
+ "github.com/anchore/clio"
hashiVersion "github.com/anchore/go-version"
- "github.com/anchore/grype/internal"
+ "github.com/anchore/grype/cmd/grype/internal"
)
var latestAppVersionURL = struct {
@@ -15,21 +16,27 @@ var latestAppVersionURL = struct {
path string
}{
host: "https://toolbox-data.anchore.io",
- path: fmt.Sprintf("/%s/releases/latest/VERSION", internal.ApplicationName),
+ path: "/grype/releases/latest/VERSION",
}
-func IsUpdateAvailable() (bool, string, error) {
- currentBuildInfo := FromBuild()
- if !currentBuildInfo.isProductionBuild() {
+func isProductionBuild(version string) bool {
+ if strings.Contains(version, "SNAPSHOT") || strings.Contains(version, internal.NotProvided) {
+ return false
+ }
+ return true
+}
+
+func isUpdateAvailable(id clio.Identification) (bool, string, error) {
+ if !isProductionBuild(id.Version) {
// don't allow for non-production builds to check for a version.
return false, "", nil
}
- currentVersion, err := hashiVersion.NewVersion(currentBuildInfo.Version)
+ currentVersion, err := hashiVersion.NewVersion(id.Version)
if err != nil {
return false, "", fmt.Errorf("failed to parse current application version: %w", err)
}
- latestVersion, err := fetchLatestApplicationVersion()
+ latestVersion, err := fetchLatestApplicationVersion(id)
if err != nil {
return false, "", err
}
@@ -41,12 +48,14 @@ func IsUpdateAvailable() (bool, string, error) {
return false, "", nil
}
-func fetchLatestApplicationVersion() (*hashiVersion.Version, error) {
+func fetchLatestApplicationVersion(id clio.Identification) (*hashiVersion.Version, error) {
req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for latest version: %w", err)
}
+ req.Header.Add("User-Agent", fmt.Sprintf("%v %v", id.Name, id.Version))
+
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
diff --git a/internal/version/update_test.go b/cmd/grype/cli/commands/update_test.go
similarity index 82%
rename from internal/version/update_test.go
rename to cmd/grype/cli/commands/update_test.go
index 10c34b7383a..e5b85e47ece 100644
--- a/internal/version/update_test.go
+++ b/cmd/grype/cli/commands/update_test.go
@@ -1,11 +1,13 @@
-package version
+package commands
import (
"net/http"
"net/http/httptest"
"testing"
+ "github.com/anchore/clio"
hashiVersion "github.com/anchore/go-version"
+ "github.com/anchore/grype/cmd/grype/internal"
)
func TestIsUpdateAvailable(t *testing.T) {
@@ -74,7 +76,7 @@ func TestIsUpdateAvailable(t *testing.T) {
},
{
name: "NoBuildVersion",
- buildVersion: valueNotProvided,
+ buildVersion: internal.NotProvided,
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
@@ -96,7 +98,7 @@ func TestIsUpdateAvailable(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
// setup mocks
// local...
- version = test.buildVersion
+ version := test.buildVersion
// remote...
handler := http.NewServeMux()
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
@@ -107,7 +109,7 @@ func TestIsUpdateAvailable(t *testing.T) {
latestAppVersionURL.host = mockSrv.URL
defer mockSrv.Close()
- isAvailable, newVersion, err := IsUpdateAvailable()
+ isAvailable, newVersion, err := isUpdateAvailable(clio.Identification{Version: version})
if err != nil && !test.err {
t.Fatalf("got error but expected none: %+v", err)
} else if err == nil && test.err {
@@ -189,7 +191,7 @@ func TestFetchLatestApplicationVersion(t *testing.T) {
latestAppVersionURL.host = mockSrv.URL
defer mockSrv.Close()
- actual, err := fetchLatestApplicationVersion()
+ actual, err := fetchLatestApplicationVersion(clio.Identification{})
if err != nil && !test.err {
t.Fatalf("got error but expected none: %+v", err)
} else if err == nil && test.err {
@@ -207,3 +209,27 @@ func TestFetchLatestApplicationVersion(t *testing.T) {
}
}
+
+func Test_UserAgent(t *testing.T) {
+ got := ""
+
+ // setup mock
+ handler := http.NewServeMux()
+ handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
+ got = r.Header.Get("User-Agent")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte("1.0.0"))
+ })
+ mockSrv := httptest.NewServer(handler)
+ latestAppVersionURL.host = mockSrv.URL
+ defer mockSrv.Close()
+
+ fetchLatestApplicationVersion(clio.Identification{
+ Name: "the-app",
+ Version: "v3.2.1",
+ })
+
+ if got != "the-app v3.2.1" {
+ t.Errorf("expected User-Agent header to match, got: %v", got)
+ }
+}
diff --git a/cmd/grype/cli/commands/util.go b/cmd/grype/cli/commands/util.go
new file mode 100644
index 00000000000..0a60dec73aa
--- /dev/null
+++ b/cmd/grype/cli/commands/util.go
@@ -0,0 +1,151 @@
+package commands
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "sync"
+
+ "github.com/hashicorp/go-multierror"
+ "github.com/olekukonko/tablewriter"
+ "github.com/spf13/cobra"
+ "golang.org/x/exp/maps"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/internal/ui"
+)
+
+func disableUI(app clio.Application) func(*cobra.Command, []string) error {
+ return func(_ *cobra.Command, _ []string) error {
+ type Stater interface {
+ State() *clio.State
+ }
+
+ state := app.(Stater).State()
+ state.UI = clio.NewUICollection(ui.None(state.Config.Log.Quiet))
+
+ return nil
+ }
+}
+
+func stderrPrintLnf(message string, args ...interface{}) error {
+ if !strings.HasSuffix(message, "\n") {
+ message += "\n"
+ }
+ _, err := fmt.Fprintf(os.Stderr, message, args...)
+ return err
+}
+
+// parallel takes a set of functions and runs them in parallel, capturing all errors returned and
+// returning the single error returned by one of the parallel funcs, or a multierror.Error with all
+// the errors if more than one
+func parallel(funcs ...func() error) error {
+ errs := parallelMapped(funcs...)
+ if len(errs) > 0 {
+ values := maps.Values(errs)
+ if len(values) == 1 {
+ return values[0]
+ }
+ return multierror.Append(nil, values...)
+ }
+ return nil
+}
+
+// parallelMapped takes a set of functions and runs them in parallel, capturing all errors returned in
+// a map indicating which func, by index returned which error
+func parallelMapped(funcs ...func() error) map[int]error {
+ errs := map[int]error{}
+ errorLock := &sync.Mutex{}
+ wg := &sync.WaitGroup{}
+ wg.Add(len(funcs))
+ for i, fn := range funcs {
+ go func(i int, fn func() error) {
+ defer wg.Done()
+ err := fn()
+ if err != nil {
+ errorLock.Lock()
+ defer errorLock.Unlock()
+ errs[i] = err
+ }
+ }(i, fn)
+ }
+ wg.Wait()
+ return errs
+}
+
+func appendErrors(errs error, err ...error) error {
+ if errs == nil {
+ switch len(err) {
+ case 0:
+ return nil
+ case 1:
+ return err[0]
+ }
+ }
+ return multierror.Append(errs, err...)
+}
+
+func newTable(output io.Writer) *tablewriter.Table {
+ // we use a trimming writer to ensure that the table is not padded with spaces when there is a single long row
+ // and several short rows. AFAICT there is no table setting to control this behavior. Why do it as a writer? So
+ // we don't need to buffer the entire table in memory before writing it out.
+ table := tablewriter.NewWriter(newTrimmingWriter(output))
+ table.SetAutoWrapText(false)
+ table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
+ table.SetAlignment(tablewriter.ALIGN_LEFT)
+
+ table.SetHeaderLine(false)
+ table.SetBorder(false)
+ table.SetAutoFormatHeaders(true)
+ table.SetCenterSeparator("")
+ table.SetColumnSeparator("")
+ table.SetRowSeparator("")
+ table.SetTablePadding(" ")
+ table.SetNoWhiteSpace(true)
+ return table
+}
+
+// trimmingWriter is a writer that trims whitespace from the end of each line. It is assumed that whole lines are
+// passed to Write() calls (no partial lines).
+type trimmingWriter struct {
+ output io.Writer
+ buffer bytes.Buffer
+}
+
+func newTrimmingWriter(w io.Writer) *trimmingWriter {
+ return &trimmingWriter{output: w}
+}
+
+func (tw *trimmingWriter) Write(p []byte) (int, error) {
+ for _, b := range p {
+ switch b {
+ case '\n':
+ // write a newline and discard any buffered spaces
+ _, err := tw.output.Write([]byte{'\n'})
+ if err != nil {
+ return 0, err
+ }
+ tw.buffer.Reset()
+ case ' ', '\t':
+ // buffer spaces and tabs
+ tw.buffer.WriteByte(b)
+ default:
+ // write any buffered spaces, then the non-whitespace character
+ if tw.buffer.Len() > 0 {
+ _, err := tw.output.Write(tw.buffer.Bytes())
+ if err != nil {
+ return 0, err
+ }
+ tw.buffer.Reset()
+ }
+ _, err := tw.output.Write([]byte{b})
+ if err != nil {
+ return 0, err
+ }
+ }
+ }
+
+ return len(p), nil
+}
diff --git a/cmd/grype/cli/commands/util_test.go b/cmd/grype/cli/commands/util_test.go
new file mode 100644
index 00000000000..8f868708701
--- /dev/null
+++ b/cmd/grype/cli/commands/util_test.go
@@ -0,0 +1,223 @@
+package commands
+
+import (
+ "bytes"
+ "fmt"
+ "sync"
+ "sync/atomic"
+ "testing"
+
+ "github.com/hashicorp/go-multierror"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+const lotsaParallel = 100
+
+func Test_lotsaLotsaParallel(t *testing.T) {
+ funcs := []func() error{}
+ for i := 0; i < lotsaParallel; i++ {
+ funcs = append(funcs, func() error {
+ Test_lotsaParallel(t)
+ return nil
+ })
+ }
+ err := parallel(funcs...)
+ require.NoError(t, err)
+}
+
+func Test_lotsaParallel(t *testing.T) {
+ for i := 0; i < lotsaParallel; i++ {
+ Test_parallel(t)
+ }
+}
+
+// Test_parallel tests the parallel function by executing a set of functions that can only execute in a specific
+// order if they are actually running in parallel.
+func Test_parallel(t *testing.T) {
+ count := atomic.Int32{}
+ count.Store(0)
+
+ wg1 := sync.WaitGroup{}
+ wg1.Add(1)
+
+ wg2 := sync.WaitGroup{}
+ wg2.Add(1)
+
+ wg3 := sync.WaitGroup{}
+ wg3.Add(1)
+
+ err1 := fmt.Errorf("error-1")
+ err2 := fmt.Errorf("error-2")
+ err3 := fmt.Errorf("error-3")
+
+ order := ""
+
+ got := parallel(
+ func() error {
+ wg1.Wait()
+ count.Add(1)
+ order = order + "_0"
+ return nil
+ },
+ func() error {
+ wg3.Wait()
+ defer wg2.Done()
+ count.Add(10)
+ order = order + "_1"
+ return err1
+ },
+ func() error {
+ wg2.Wait()
+ defer wg1.Done()
+ count.Add(100)
+ order = order + "_2"
+ return err2
+ },
+ func() error {
+ defer wg3.Done()
+ count.Add(1000)
+ order = order + "_3"
+ return err3
+ },
+ )
+ require.Equal(t, int32(1111), count.Load())
+ require.Equal(t, "_3_1_2_0", order)
+
+ errs := got.(*multierror.Error).Errors
+
+ // cannot check equality to a slice with err1,2,3 because the functions above are running in parallel, for example:
+ // after func()#4 returns and the `wg3.Done()` has executed, the thread could immediately pause
+ // and the remaining functions execute first and err3 becomes the last in the list instead of the first
+ require.Contains(t, errs, err1)
+ require.Contains(t, errs, err2)
+ require.Contains(t, errs, err3)
+}
+
+func Test_parallelMapped(t *testing.T) {
+ err0 := fmt.Errorf("error-0")
+ err1 := fmt.Errorf("error-1")
+ err2 := fmt.Errorf("error-2")
+
+ tests := []struct {
+ name string
+ funcs []func() error
+ expected map[int]error
+ }{
+ {
+ name: "basic",
+ funcs: []func() error{
+ func() error {
+ return nil
+ },
+ func() error {
+ return err1
+ },
+ func() error {
+ return nil
+ },
+ func() error {
+ return err2
+ },
+ },
+ expected: map[int]error{
+ 1: err1,
+ 3: err2,
+ },
+ },
+ {
+ name: "no errors",
+ funcs: []func() error{
+ func() error {
+ return nil
+ },
+ func() error {
+ return nil
+ },
+ },
+ expected: map[int]error{},
+ },
+ {
+ name: "all errors",
+ funcs: []func() error{
+ func() error {
+ return err0
+ },
+ func() error {
+ return err1
+ },
+ func() error {
+ return err2
+ },
+ },
+ expected: map[int]error{
+ 0: err0,
+ 1: err1,
+ 2: err2,
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got := parallelMapped(test.funcs...)
+ require.Equal(t, test.expected, got)
+ })
+ }
+}
+
+func TestTrimmingWriter(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "removes trailing spaces",
+ input: "line with trailing spaces \nline with no trailing spaces\n",
+ expected: "line with trailing spaces\nline with no trailing spaces\n",
+ },
+ {
+ name: "handles multiple spaces and tabs",
+ input: "line with tabs\t\t\t\nline with spaces \t \t\t\n",
+ expected: "line with tabs\nline with spaces\n",
+ },
+ {
+ name: "handles embedded whitespace",
+ input: "line one with spaces and tabs\t\t\nnext line\t\n",
+ expected: "line one with spaces and tabs\nnext line\n",
+ },
+ {
+ name: "handles empty input",
+ input: "",
+ expected: "",
+ },
+ {
+ name: "handles only spaces and tabs",
+ input: " \t\t\n \t \t\n",
+ expected: "\n\n",
+ },
+ {
+ name: "handles single character input",
+ input: "a",
+ expected: "a",
+ },
+ {
+ name: "handles input ending without newline",
+ input: "line without newline ",
+ expected: "line without newline",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var output bytes.Buffer
+ writer := newTrimmingWriter(&output)
+
+ n, err := writer.Write([]byte(tt.input))
+ assert.NoError(t, err)
+ assert.Equal(t, len(tt.input), n)
+ assert.Equal(t, tt.expected, output.String())
+ })
+ }
+}
diff --git a/cmd/grype/cli/legacy/cmd.go b/cmd/grype/cli/legacy/cmd.go
deleted file mode 100644
index 9c5520c2db5..00000000000
--- a/cmd/grype/cli/legacy/cmd.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package legacy
-
-import (
- "encoding/json"
- "fmt"
- "os"
- "sort"
-
- "github.com/gookit/color"
- logrusUpstream "github.com/sirupsen/logrus"
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
- "github.com/wagoodman/go-partybus"
-
- "github.com/anchore/go-logger/adapter/logrus"
- "github.com/anchore/grype/grype"
- "github.com/anchore/grype/internal/config"
- "github.com/anchore/grype/internal/log"
- "github.com/anchore/grype/internal/version"
- "github.com/anchore/stereoscope"
- "github.com/anchore/syft/syft"
-)
-
-var (
- appConfig *config.Application
- eventBus *partybus.Bus
- eventSubscription *partybus.Subscription
-)
-
-func init() {
- cobra.OnInitialize(
- initRootCmdConfigOptions,
- initAppConfig,
- initLogging,
- logAppConfig,
- logAppVersion,
- initEventBus,
- )
-}
-
-func Execute() {
- if err := rootCmd.Execute(); err != nil {
- _ = stderrPrintLnf(err.Error())
- os.Exit(1)
- }
-}
-
-func initRootCmdConfigOptions() {
- if err := bindRootConfigOptions(rootCmd.Flags()); err != nil {
- panic(err)
- }
-}
-
-func initAppConfig() {
- cfg, err := config.LoadApplicationConfig(viper.GetViper(), persistentOpts)
- if err != nil {
- fmt.Printf("failed to load application config: \n\t%+v\n", err)
- os.Exit(1)
- }
- appConfig = cfg
-}
-
-func initLogging() {
- cfg := logrus.Config{
- EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.CliOptions.Verbosity > 0) && !appConfig.Quiet,
- FileLocation: appConfig.Log.FileLocation,
- Level: appConfig.Log.Level,
- }
-
- if appConfig.Log.Structured {
- cfg.Formatter = &logrusUpstream.JSONFormatter{
- TimestampFormat: "2006-01-02T15:04:05.000Z",
- DisableTimestamp: false,
- DisableHTMLEscape: false,
- PrettyPrint: false,
- }
- }
-
- logWrapper, err := logrus.New(cfg)
- if err != nil {
- // this is kinda circular, but we can't return an error... ¯\_(ツ)_/¯
- // I'm going to leave this here in case we one day have a different default logger other than the "discard" logger
- log.Error("unable to initialize logger: %+v", err)
- return
- }
- grype.SetLogger(logWrapper)
- syft.SetLogger(logWrapper.Nested("from-lib", "syft"))
- stereoscope.SetLogger(logWrapper.Nested("from-lib", "stereoscope"))
-}
-
-func logAppConfig() {
- log.Debugf("application config:\n%+v", color.Magenta.Sprint(appConfig.String()))
-}
-
-func logAppVersion() {
- versionInfo := version.FromBuild()
- log.Infof("grype version: %s", versionInfo.Version)
-
- var fields map[string]interface{}
- bytes, err := json.Marshal(versionInfo)
- if err != nil {
- return
- }
- err = json.Unmarshal(bytes, &fields)
- if err != nil {
- return
- }
-
- keys := make([]string, 0, len(fields))
- for k := range fields {
- keys = append(keys, k)
- }
- sort.Strings(keys)
-
- for idx, field := range keys {
- value := fields[field]
- branch := "├──"
- if idx == len(fields)-1 {
- branch = "└──"
- }
- log.Debugf(" %s %s: %s", branch, field, value)
- }
-}
-
-func initEventBus() {
- eventBus = partybus.NewBus()
- eventSubscription = eventBus.Subscribe()
-
- stereoscope.SetBus(eventBus)
- syft.SetBus(eventBus)
- grype.SetBus(eventBus)
-}
diff --git a/cmd/grype/cli/legacy/db.go b/cmd/grype/cli/legacy/db.go
deleted file mode 100644
index 247ed7c5ca5..00000000000
--- a/cmd/grype/cli/legacy/db.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package legacy
-
-import (
- "github.com/spf13/cobra"
-)
-
-var dbCmd = &cobra.Command{
- Use: "db",
- Short: "vulnerability database operations",
-}
-
-func init() {
- rootCmd.AddCommand(dbCmd)
-}
diff --git a/cmd/grype/cli/legacy/db_check.go b/cmd/grype/cli/legacy/db_check.go
deleted file mode 100644
index 0c1eec9822f..00000000000
--- a/cmd/grype/cli/legacy/db_check.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package legacy
-
-import (
- "fmt"
-
- "github.com/spf13/cobra"
-
- "github.com/anchore/grype/grype/db"
-)
-
-var dbCheckCmd = &cobra.Command{
- Use: "check",
- Short: "check to see if there is a database update available",
- Args: cobra.ExactArgs(0),
- RunE: runDBCheckCmd,
-}
-
-func init() {
- dbCmd.AddCommand(dbCheckCmd)
-}
-
-func runDBCheckCmd(_ *cobra.Command, _ []string) error {
- dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig())
- if err != nil {
- return err
- }
-
- updateAvailable, currentDBMetadata, updateDBEntry, err := dbCurator.IsUpdateAvailable()
- if err != nil {
- return fmt.Errorf("unable to check for vulnerability database update: %+v", err)
- }
-
- if !updateAvailable {
- return stderrPrintLnf("No update available")
- }
-
- fmt.Println("Update available!")
-
- if currentDBMetadata != nil {
- fmt.Printf("Current DB version %d was built on %s\n", currentDBMetadata.Version, currentDBMetadata.Built.String())
- }
-
- fmt.Printf("Updated DB version %d was built on %s\n", updateDBEntry.Version, updateDBEntry.Built.String())
- fmt.Printf("Updated DB URL: %s\n", updateDBEntry.URL.String())
- fmt.Println("You can run 'grype db update' to update to the latest db")
-
- return nil
-}
diff --git a/cmd/grype/cli/legacy/db_delete.go b/cmd/grype/cli/legacy/db_delete.go
deleted file mode 100644
index 4ca36db32a0..00000000000
--- a/cmd/grype/cli/legacy/db_delete.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package legacy
-
-import (
- "fmt"
-
- "github.com/spf13/cobra"
-
- "github.com/anchore/grype/grype/db"
-)
-
-var dbDeleteCmd = &cobra.Command{
- Use: "delete",
- Short: "delete the vulnerability database",
- Args: cobra.ExactArgs(0),
- RunE: runDBDeleteCmd,
-}
-
-func init() {
- dbCmd.AddCommand(dbDeleteCmd)
-}
-
-func runDBDeleteCmd(_ *cobra.Command, _ []string) error {
- dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig())
- if err != nil {
- return err
- }
-
- if err := dbCurator.Delete(); err != nil {
- return fmt.Errorf("unable to delete vulnerability database: %+v", err)
- }
-
- return stderrPrintLnf("Vulnerability database deleted")
-}
diff --git a/cmd/grype/cli/legacy/db_diff.go b/cmd/grype/cli/legacy/db_diff.go
deleted file mode 100644
index 180ec99ed58..00000000000
--- a/cmd/grype/cli/legacy/db_diff.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package legacy
-
-import (
- "strings"
-
- "github.com/spf13/cobra"
-
- "github.com/anchore/grype/cmd/grype/internal/ui"
- "github.com/anchore/grype/grype/db"
- "github.com/anchore/grype/grype/differ"
- "github.com/anchore/grype/internal/bus"
- "github.com/anchore/grype/internal/log"
- "github.com/anchore/stereoscope"
-)
-
-var dbDiffOutputFormat string
-
-const deleteFlag string = "delete"
-
-var dbDiffCmd = &cobra.Command{
- Use: "diff [flags] base_db_url target_db_url",
- Short: "diff two DBs and display the result",
- Args: cobra.MaximumNArgs(2),
- RunE: runDBDiffCmd,
-}
-
-func init() {
- dbDiffCmd.Flags().StringVarP(&dbDiffOutputFormat, "output", "o", "table", "format to display results (available=[table, json])")
- dbDiffCmd.Flags().BoolP(deleteFlag, "d", false, "delete downloaded databases after diff occurs")
-
- dbCmd.AddCommand(dbDiffCmd)
-}
-
-func startDBDiffCmd(base string, target string, deleteDatabases bool) <-chan error {
- errs := make(chan error)
- go func() {
- defer close(errs)
- defer bus.Exit()
- d, err := differ.NewDiffer(appConfig.DB.ToCuratorConfig())
- if err != nil {
- errs <- err
- return
- }
-
- if err := d.SetBaseDB(base); err != nil {
- errs <- err
- return
- }
-
- if err := d.SetTargetDB(target); err != nil {
- errs <- err
- return
- }
-
- diff, err := d.DiffDatabases()
- if err != nil {
- errs <- err
- return
- }
-
- sb := &strings.Builder{}
-
- if len(*diff) == 0 {
- sb.WriteString("Databases are identical!\n")
- } else {
- err := d.Present(dbDiffOutputFormat, diff, sb)
- if err != nil {
- errs <- err
- }
- }
-
- bus.Report(sb.String())
-
- if deleteDatabases {
- errs <- d.DeleteDatabases()
- }
- }()
- return errs
-}
-
-func runDBDiffCmd(cmd *cobra.Command, args []string) error {
- deleteDatabases, err := cmd.Flags().GetBool(deleteFlag)
- if err != nil {
- return err
- }
-
- var base, target string
-
- switch len(args) {
- case 0:
- log.Info("base_db_url and target_db_url not provided; fetching most recent")
- base, target, err = getDefaultURLs()
- if err != nil {
- return err
- }
- case 1:
- log.Info("target_db_url not provided; fetching most recent")
- base = args[0]
- _, target, err = getDefaultURLs()
- if err != nil {
- return err
- }
- default:
- base = args[0]
- target = args[1]
- }
-
- return eventLoop(
- startDBDiffCmd(base, target, deleteDatabases),
- setupSignals(),
- eventSubscription,
- stereoscope.Cleanup,
- ui.Select(isVerbose(), appConfig.Quiet)...,
- )
-}
-
-func getDefaultURLs() (baseURL string, targetURL string, err error) {
- dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig())
- if err != nil {
- return "", "", err
- }
-
- listing, err := dbCurator.ListingFromURL()
- if err != nil {
- return "", "", err
- }
-
- supportedSchema := dbCurator.SupportedSchema()
- available, exists := listing.Available[supportedSchema]
- if len(available) < 2 || !exists {
- return "", "", stderrPrintLnf("Not enough databases available for the current schema to diff (%d)", supportedSchema)
- }
-
- targetURL = available[0].URL.String()
- baseURL = available[1].URL.String()
-
- return baseURL, targetURL, nil
-}
diff --git a/cmd/grype/cli/legacy/db_import.go b/cmd/grype/cli/legacy/db_import.go
deleted file mode 100644
index 400d19d15a9..00000000000
--- a/cmd/grype/cli/legacy/db_import.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package legacy
-
-import (
- "fmt"
-
- "github.com/spf13/cobra"
-
- "github.com/anchore/grype/grype/db"
- "github.com/anchore/grype/internal"
-)
-
-var dbImportCmd = &cobra.Command{
- Use: "import FILE",
- Short: "import a vulnerability database archive",
- Long: fmt.Sprintf("import a vulnerability database archive from a local FILE.\nDB archives can be obtained from %q.", internal.DBUpdateURL),
- Args: cobra.ExactArgs(1),
- RunE: runDBImportCmd,
-}
-
-func init() {
- dbCmd.AddCommand(dbImportCmd)
-}
-
-func runDBImportCmd(_ *cobra.Command, args []string) error {
- dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig())
- if err != nil {
- return err
- }
-
- if err := dbCurator.ImportFrom(args[0]); err != nil {
- return fmt.Errorf("unable to import vulnerability database: %+v", err)
- }
-
- return stderrPrintLnf("Vulnerability database imported")
-}
diff --git a/cmd/grype/cli/legacy/db_list.go b/cmd/grype/cli/legacy/db_list.go
deleted file mode 100644
index e07240b6595..00000000000
--- a/cmd/grype/cli/legacy/db_list.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package legacy
-
-import (
- "encoding/json"
- "fmt"
- "os"
-
- "github.com/spf13/cobra"
-
- "github.com/anchore/grype/grype/db"
-)
-
-var dbListOutputFormat string
-
-var dbListCmd = &cobra.Command{
- Use: "list",
- Short: "list all DBs available according to the listing URL",
- Args: cobra.ExactArgs(0),
- RunE: runDBListCmd,
-}
-
-func init() {
- dbListCmd.Flags().StringVarP(&dbListOutputFormat, "output", "o", "text", "format to display results (available=[text, raw, json])")
-
- dbCmd.AddCommand(dbListCmd)
-}
-
-func runDBListCmd(_ *cobra.Command, _ []string) error {
- dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig())
- if err != nil {
- return err
- }
-
- listing, err := dbCurator.ListingFromURL()
- if err != nil {
- return err
- }
-
- supportedSchema := dbCurator.SupportedSchema()
- available, exists := listing.Available[supportedSchema]
-
- if len(available) == 0 || !exists {
- return stderrPrintLnf("No databases available for the current schema (%d)", supportedSchema)
- }
-
- switch dbListOutputFormat {
- case "text":
- // summarize each listing entry for the current DB schema
- for _, l := range available {
- fmt.Printf("Built: %s\n", l.Built)
- fmt.Printf("URL: %s\n", l.URL)
- fmt.Printf("Checksum: %s\n\n", l.Checksum)
- }
-
- fmt.Printf("%d databases available for schema %d\n", len(available), supportedSchema)
- case "json":
- // show entries for the current schema
- enc := json.NewEncoder(os.Stdout)
- enc.SetEscapeHTML(false)
- enc.SetIndent("", " ")
- if err := enc.Encode(&available); err != nil {
- return fmt.Errorf("failed to db listing information: %+v", err)
- }
- case "raw":
- // show the entire listing file
- enc := json.NewEncoder(os.Stdout)
- enc.SetEscapeHTML(false)
- enc.SetIndent("", " ")
- if err := enc.Encode(&listing); err != nil {
- return fmt.Errorf("failed to db listing information: %+v", err)
- }
- default:
- return fmt.Errorf("unsupported output format: %s", dbListOutputFormat)
- }
-
- return nil
-}
diff --git a/cmd/grype/cli/legacy/db_status.go b/cmd/grype/cli/legacy/db_status.go
deleted file mode 100644
index 4ed655f67e9..00000000000
--- a/cmd/grype/cli/legacy/db_status.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package legacy
-
-import (
- "fmt"
-
- "github.com/spf13/cobra"
-
- "github.com/anchore/grype/grype/db"
-)
-
-var statusCmd = &cobra.Command{
- Use: "status",
- Short: "display database status",
- Args: cobra.ExactArgs(0),
- RunE: runDBStatusCmd,
-}
-
-func init() {
- dbCmd.AddCommand(statusCmd)
-}
-
-func runDBStatusCmd(_ *cobra.Command, _ []string) error {
- dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig())
- if err != nil {
- return err
- }
-
- status := dbCurator.Status()
-
- statusStr := "valid"
- if status.Err != nil {
- statusStr = "invalid"
- }
-
- fmt.Println("Location: ", status.Location)
- fmt.Println("Built: ", status.Built.String())
- fmt.Println("Schema: ", status.SchemaVersion)
- fmt.Println("Checksum: ", status.Checksum)
- fmt.Println("Status: ", statusStr)
-
- return status.Err
-}
diff --git a/cmd/grype/cli/legacy/db_update.go b/cmd/grype/cli/legacy/db_update.go
deleted file mode 100644
index 1a72ce6dacf..00000000000
--- a/cmd/grype/cli/legacy/db_update.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package legacy
-
-import (
- "fmt"
-
- "github.com/spf13/cobra"
-
- "github.com/anchore/grype/cmd/grype/internal/ui"
- "github.com/anchore/grype/grype/db"
- "github.com/anchore/grype/internal/bus"
- "github.com/anchore/stereoscope"
-)
-
-var dbUpdateCmd = &cobra.Command{
- Use: "update",
- Short: "download the latest vulnerability database",
- Args: cobra.ExactArgs(0),
- RunE: runDBUpdateCmd,
-}
-
-func init() {
- dbCmd.AddCommand(dbUpdateCmd)
-}
-
-func startDBUpdateCmd() <-chan error {
- errs := make(chan error)
- go func() {
- defer close(errs)
- defer bus.Exit()
-
- dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig())
- if err != nil {
- errs <- err
- return
- }
- updated, err := dbCurator.Update()
- if err != nil {
- errs <- fmt.Errorf("unable to update vulnerability database: %+v", err)
- }
-
- result := "No vulnerability database update available\n"
- if updated {
- result = "Vulnerability database updated to latest version!\n"
- }
-
- bus.Report(result)
- }()
- return errs
-}
-
-func runDBUpdateCmd(_ *cobra.Command, _ []string) error {
- return eventLoop(
- startDBUpdateCmd(),
- setupSignals(),
- eventSubscription,
- stereoscope.Cleanup,
- ui.Select(isVerbose(), appConfig.Quiet)...,
- )
-}
diff --git a/cmd/grype/cli/legacy/event_loop.go b/cmd/grype/cli/legacy/event_loop.go
deleted file mode 100644
index 51faa023175..00000000000
--- a/cmd/grype/cli/legacy/event_loop.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package legacy
-
-import (
- "errors"
- "fmt"
- "os"
-
- "github.com/hashicorp/go-multierror"
- "github.com/wagoodman/go-partybus"
-
- "github.com/anchore/clio"
- "github.com/anchore/grype/internal/log"
-)
-
-// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
-// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
-// an eventual graceful exit.
-func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...clio.UI) error {
- defer cleanupFn()
- events := subscription.Events()
- var err error
- var ux clio.UI
-
- if ux, err = setupUI(subscription, uxs...); err != nil {
- return err
- }
-
- var retErr error
- var forceTeardown bool
-
- for {
- if workerErrs == nil && events == nil {
- break
- }
- select {
- case err, isOpen := <-workerErrs:
- if !isOpen {
- workerErrs = nil
- continue
- }
- if err != nil {
- // capture the error from the worker and unsubscribe to complete a graceful shutdown
- retErr = multierror.Append(retErr, err)
- _ = subscription.Unsubscribe()
- // the worker has exited, we may have been mid-handling events for the UI which should now be
- // ignored, in which case forcing a teardown of the UI irregardless of the state is required.
- forceTeardown = true
- }
- case e, isOpen := <-events:
- if !isOpen {
- events = nil
- continue
- }
-
- if err := ux.Handle(e); err != nil {
- if errors.Is(err, partybus.ErrUnsubscribe) {
- events = nil
- } else {
- retErr = multierror.Append(retErr, err)
- // TODO: should we unsubscribe? should we try to halt execution? or continue?
- }
- }
- case <-signals:
- // ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up.
- // we ignore further errors since cleaning up the tmp directories will affect running catalogers that are
- // reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result.
-
- // TODO: potential future improvement would be to pass context into workers with a cancel function that is
- // to the event loop. In this way we can have a more controlled shutdown even at the most nested levels
- // of processing.
- events = nil
- workerErrs = nil
- forceTeardown = true
- }
- }
-
- if err := ux.Teardown(forceTeardown); err != nil {
- retErr = multierror.Append(retErr, err)
- }
-
- return retErr
-}
-
-// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use
-// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error
-// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks
-// when there are environmental problem (e.g. unable to setup a TUI with the current TTY).
-func setupUI(subscription *partybus.Subscription, uis ...clio.UI) (clio.UI, error) {
- for _, ux := range uis {
- if err := ux.Setup(subscription); err != nil {
- log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
- continue
- }
-
- return ux, nil
- }
- return nil, fmt.Errorf("unable to setup any UI")
-}
diff --git a/cmd/grype/cli/legacy/event_loop_test.go b/cmd/grype/cli/legacy/event_loop_test.go
deleted file mode 100644
index 489065d3761..00000000000
--- a/cmd/grype/cli/legacy/event_loop_test.go
+++ /dev/null
@@ -1,459 +0,0 @@
-package legacy
-
-import (
- "fmt"
- "os"
- "syscall"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/wagoodman/go-partybus"
-
- "github.com/anchore/clio"
- "github.com/anchore/grype/grype/event"
-)
-
-var _ clio.UI = (*uiMock)(nil)
-
-type uiMock struct {
- t *testing.T
- finalEvent partybus.Event
- subscription partybus.Unsubscribable
- mock.Mock
-}
-
-func (u *uiMock) Setup(unsubscribe partybus.Unsubscribable) error {
- u.t.Helper()
- u.t.Logf("UI Setup called")
- u.subscription = unsubscribe
- return u.Called(unsubscribe.Unsubscribe).Error(0)
-}
-
-func (u *uiMock) Handle(event partybus.Event) error {
- u.t.Helper()
- u.t.Logf("UI Handle called: %+v", event.Type)
- if event == u.finalEvent {
- assert.NoError(u.t, u.subscription.Unsubscribe())
- }
- return u.Called(event).Error(0)
-}
-
-func (u *uiMock) Teardown(_ bool) error {
- u.t.Helper()
- u.t.Logf("UI Teardown called")
- return u.Called().Error(0)
-}
-
-func Test_EventLoop_gracefulExit(t *testing.T) {
- test := func(t *testing.T) {
-
- testBus := partybus.NewBus()
- subscription := testBus.Subscribe()
- t.Cleanup(testBus.Close)
-
- finalEvent := partybus.Event{
- Type: event.CLIExit,
- }
-
- worker := func() <-chan error {
- ret := make(chan error)
- go func() {
- t.Log("worker running")
- // send an empty item (which is ignored) ensuring we've entered the select statement,
- // then close (a partial shutdown).
- ret <- nil
- t.Log("worker sent nothing")
- close(ret)
- t.Log("worker closed")
- // do the other half of the shutdown
- testBus.Publish(finalEvent)
- t.Log("worker published final event")
- }()
- return ret
- }
-
- signaler := func() <-chan os.Signal {
- return nil
- }
-
- ux := &uiMock{
- t: t,
- finalEvent: finalEvent,
- }
-
- // ensure the mock sees at least the final event
- ux.On("Handle", finalEvent).Return(nil)
- // ensure the mock sees basic setup/teardown events
- ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
- ux.On("Teardown").Return(nil)
-
- var cleanupCalled bool
- cleanupFn := func() {
- t.Log("cleanup called")
- cleanupCalled = true
- }
-
- assert.NoError(t,
- eventLoop(
- worker(),
- signaler(),
- subscription,
- cleanupFn,
- ux,
- ),
- )
-
- assert.True(t, cleanupCalled, "cleanup function not called")
- ux.AssertExpectations(t)
- }
-
- // if there is a bug, then there is a risk of the event loop never returning
- testWithTimeout(t, 5*time.Second, test)
-}
-
-func Test_EventLoop_workerError(t *testing.T) {
- test := func(t *testing.T) {
-
- testBus := partybus.NewBus()
- subscription := testBus.Subscribe()
- t.Cleanup(testBus.Close)
-
- workerErr := fmt.Errorf("worker error")
-
- worker := func() <-chan error {
- ret := make(chan error)
- go func() {
- t.Log("worker running")
- // send an empty item (which is ignored) ensuring we've entered the select statement,
- // then close (a partial shutdown).
- ret <- nil
- t.Log("worker sent nothing")
- ret <- workerErr
- t.Log("worker sent error")
- close(ret)
- t.Log("worker closed")
- // note: NO final event is fired
- }()
- return ret
- }
-
- signaler := func() <-chan os.Signal {
- return nil
- }
-
- ux := &uiMock{
- t: t,
- }
-
- // ensure the mock sees basic setup/teardown events
- ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
- ux.On("Teardown").Return(nil)
-
- var cleanupCalled bool
- cleanupFn := func() {
- t.Log("cleanup called")
- cleanupCalled = true
- }
-
- // ensure we see an error returned
- assert.ErrorIs(t,
- eventLoop(
- worker(),
- signaler(),
- subscription,
- cleanupFn,
- ux,
- ),
- workerErr,
- "should have seen a worker error, but did not",
- )
-
- assert.True(t, cleanupCalled, "cleanup function not called")
- ux.AssertExpectations(t)
- }
-
- // if there is a bug, then there is a risk of the event loop never returning
- testWithTimeout(t, 5*time.Second, test)
-}
-
-func Test_EventLoop_unsubscribeError(t *testing.T) {
- test := func(t *testing.T) {
-
- testBus := partybus.NewBus()
- subscription := testBus.Subscribe()
- t.Cleanup(testBus.Close)
-
- finalEvent := partybus.Event{
- Type: event.CLIExit,
- }
-
- worker := func() <-chan error {
- ret := make(chan error)
- go func() {
- t.Log("worker running")
- // send an empty item (which is ignored) ensuring we've entered the select statement,
- // then close (a partial shutdown).
- ret <- nil
- t.Log("worker sent nothing")
- close(ret)
- t.Log("worker closed")
- // do the other half of the shutdown
- testBus.Publish(finalEvent)
- t.Log("worker published final event")
- }()
- return ret
- }
-
- signaler := func() <-chan os.Signal {
- return nil
- }
-
- ux := &uiMock{
- t: t,
- finalEvent: finalEvent,
- }
-
- // ensure the mock sees at least the final event... note the unsubscribe error here
- ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe)
- // ensure the mock sees basic setup/teardown events
- ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
- ux.On("Teardown").Return(nil)
-
- var cleanupCalled bool
- cleanupFn := func() {
- t.Log("cleanup called")
- cleanupCalled = true
- }
-
- // unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that
- // this case is handled as a controlled shutdown (this test should not timeout)
- assert.NoError(t,
- eventLoop(
- worker(),
- signaler(),
- subscription,
- cleanupFn,
- ux,
- ),
- )
-
- assert.True(t, cleanupCalled, "cleanup function not called")
- ux.AssertExpectations(t)
- }
-
- // if there is a bug, then there is a risk of the event loop never returning
- testWithTimeout(t, 5*time.Second, test)
-}
-
-func Test_EventLoop_handlerError(t *testing.T) {
- test := func(t *testing.T) {
-
- testBus := partybus.NewBus()
- subscription := testBus.Subscribe()
- t.Cleanup(testBus.Close)
-
- finalEvent := partybus.Event{
- Type: event.CLIExit,
- Error: fmt.Errorf("an exit error occured"),
- }
-
- worker := func() <-chan error {
- ret := make(chan error)
- go func() {
- t.Log("worker running")
- // send an empty item (which is ignored) ensuring we've entered the select statement,
- // then close (a partial shutdown).
- ret <- nil
- t.Log("worker sent nothing")
- close(ret)
- t.Log("worker closed")
- // do the other half of the shutdown
- testBus.Publish(finalEvent)
- t.Log("worker published final event")
- }()
- return ret
- }
-
- signaler := func() <-chan os.Signal {
- return nil
- }
-
- ux := &uiMock{
- t: t,
- finalEvent: finalEvent,
- }
-
- // ensure the mock sees at least the final event... note the event error is propagated
- ux.On("Handle", finalEvent).Return(finalEvent.Error)
- // ensure the mock sees basic setup/teardown events
- ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
- ux.On("Teardown").Return(nil)
-
- var cleanupCalled bool
- cleanupFn := func() {
- t.Log("cleanup called")
- cleanupCalled = true
- }
-
- // handle errors SHOULD propagate the event loop. We are additionally asserting that this case is
- // handled as a controlled shutdown (this test should not timeout)
- assert.ErrorIs(t,
- eventLoop(
- worker(),
- signaler(),
- subscription,
- cleanupFn,
- ux,
- ),
- finalEvent.Error,
- "should have seen a event error, but did not",
- )
-
- assert.True(t, cleanupCalled, "cleanup function not called")
- ux.AssertExpectations(t)
- }
-
- // if there is a bug, then there is a risk of the event loop never returning
- testWithTimeout(t, 5*time.Second, test)
-}
-
-func Test_EventLoop_signalsStopExecution(t *testing.T) {
- test := func(t *testing.T) {
-
- testBus := partybus.NewBus()
- subscription := testBus.Subscribe()
- t.Cleanup(testBus.Close)
-
- worker := func() <-chan error {
- // the worker will never return work and the event loop will always be waiting...
- return make(chan error)
- }
-
- signaler := func() <-chan os.Signal {
- ret := make(chan os.Signal)
- go func() {
- ret <- syscall.SIGINT
- // note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit
- }()
- return ret
- }
-
- ux := &uiMock{
- t: t,
- }
-
- // ensure the mock sees basic setup/teardown events
- ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
- ux.On("Teardown").Return(nil)
-
- var cleanupCalled bool
- cleanupFn := func() {
- t.Log("cleanup called")
- cleanupCalled = true
- }
-
- assert.NoError(t,
- eventLoop(
- worker(),
- signaler(),
- subscription,
- cleanupFn,
- ux,
- ),
- )
-
- assert.True(t, cleanupCalled, "cleanup function not called")
- ux.AssertExpectations(t)
- }
-
- // if there is a bug, then there is a risk of the event loop never returning
- testWithTimeout(t, 5*time.Second, test)
-}
-
-func Test_EventLoop_uiTeardownError(t *testing.T) {
- test := func(t *testing.T) {
-
- testBus := partybus.NewBus()
- subscription := testBus.Subscribe()
- t.Cleanup(testBus.Close)
-
- finalEvent := partybus.Event{
- Type: event.CLIExit,
- }
-
- worker := func() <-chan error {
- ret := make(chan error)
- go func() {
- t.Log("worker running")
- // send an empty item (which is ignored) ensuring we've entered the select statement,
- // then close (a partial shutdown).
- ret <- nil
- t.Log("worker sent nothing")
- close(ret)
- t.Log("worker closed")
- // do the other half of the shutdown
- testBus.Publish(finalEvent)
- t.Log("worker published final event")
- }()
- return ret
- }
-
- signaler := func() <-chan os.Signal {
- return nil
- }
-
- ux := &uiMock{
- t: t,
- finalEvent: finalEvent,
- }
-
- teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down")
-
- // ensure the mock sees at least the final event... note the event error is propagated
- ux.On("Handle", finalEvent).Return(nil)
- // ensure the mock sees basic setup/teardown events
- ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
- ux.On("Teardown").Return(teardownError)
-
- var cleanupCalled bool
- cleanupFn := func() {
- t.Log("cleanup called")
- cleanupCalled = true
- }
-
- // ensure we see an error returned
- assert.ErrorIs(t,
- eventLoop(
- worker(),
- signaler(),
- subscription,
- cleanupFn,
- ux,
- ),
- teardownError,
- "should have seen a UI teardown error, but did not",
- )
-
- assert.True(t, cleanupCalled, "cleanup function not called")
- ux.AssertExpectations(t)
- }
-
- // if there is a bug, then there is a risk of the event loop never returning
- testWithTimeout(t, 5*time.Second, test)
-}
-
-func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
- done := make(chan bool)
- go func() {
- test(t)
- done <- true
- }()
-
- select {
- case <-time.After(timeout):
- t.Fatal("test timed out")
- case <-done:
- }
-}
diff --git a/cmd/grype/cli/legacy/root.go b/cmd/grype/cli/legacy/root.go
deleted file mode 100644
index 0fb0b6a18e5..00000000000
--- a/cmd/grype/cli/legacy/root.go
+++ /dev/null
@@ -1,510 +0,0 @@
-package legacy
-
-import (
- "errors"
- "fmt"
- "os"
- "strings"
- "sync"
-
- "github.com/pkg/profile"
- "github.com/spf13/cobra"
- "github.com/spf13/pflag"
- "github.com/spf13/viper"
- "github.com/wagoodman/go-partybus"
-
- "github.com/anchore/grype/cmd/grype/internal/ui"
- "github.com/anchore/grype/grype"
- "github.com/anchore/grype/grype/db"
- grypeDb "github.com/anchore/grype/grype/db/v5"
- "github.com/anchore/grype/grype/event"
- "github.com/anchore/grype/grype/grypeerr"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/matcher"
- "github.com/anchore/grype/grype/matcher/dotnet"
- "github.com/anchore/grype/grype/matcher/golang"
- "github.com/anchore/grype/grype/matcher/java"
- "github.com/anchore/grype/grype/matcher/javascript"
- "github.com/anchore/grype/grype/matcher/python"
- "github.com/anchore/grype/grype/matcher/ruby"
- "github.com/anchore/grype/grype/matcher/stock"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/presenter/models"
- "github.com/anchore/grype/grype/store"
- "github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/grype/internal"
- "github.com/anchore/grype/internal/bus"
- "github.com/anchore/grype/internal/config"
- "github.com/anchore/grype/internal/format"
- "github.com/anchore/grype/internal/log"
- "github.com/anchore/grype/internal/stringutil"
- "github.com/anchore/grype/internal/version"
- "github.com/anchore/stereoscope"
- "github.com/anchore/syft/syft/linux"
- syftPkg "github.com/anchore/syft/syft/pkg"
- "github.com/anchore/syft/syft/sbom"
- "github.com/anchore/syft/syft/source"
-)
-
-var persistentOpts = config.CliOnlyOptions{}
-
-var ignoreNonFixedMatches = []match.IgnoreRule{
- {FixState: string(grypeDb.NotFixedState)},
- {FixState: string(grypeDb.WontFixState)},
- {FixState: string(grypeDb.UnknownFixState)},
-}
-
-var ignoreFixedMatches = []match.IgnoreRule{
- {FixState: string(grypeDb.FixedState)},
-}
-
-var (
- rootCmd = &cobra.Command{
- Use: fmt.Sprintf("%s [IMAGE]", internal.ApplicationName),
- Short: "A vulnerability scanner for container images, filesystems, and SBOMs",
- Long: stringutil.Tprintf(`A vulnerability scanner for container images, filesystems, and SBOMs.
-
-Supports the following image sources:
- {{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
- {{.appName}} path/to/yourproject a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
-
-You can also explicitly specify the scheme to use:
- {{.appName}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
- {{.appName}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
- {{.appName}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
- {{.appName}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Podman or otherwise)
- {{.appName}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
- {{.appName}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
- {{.appName}} dir:path/to/yourproject read directly from a path on disk (any directory)
- {{.appName}} sbom:path/to/syft.json read Syft JSON from path on disk
- {{.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 purls from a path on disk
-
-You can also pipe in Syft JSON directly:
- syft yourimage:tag -o json | {{.appName}}
-
-`, map[string]interface{}{
- "appName": internal.ApplicationName,
- }),
- Args: validateRootArgs,
- SilenceUsage: true,
- SilenceErrors: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- if appConfig.Dev.ProfileCPU {
- defer profile.Start(profile.CPUProfile).Stop()
- } else if appConfig.Dev.ProfileMem {
- defer profile.Start(profile.MemProfile).Stop()
- }
-
- return rootExec(cmd, args)
- },
- ValidArgsFunction: dockerImageValidArgsFunction,
- }
-)
-
-func init() {
- setGlobalCliOptions()
- setRootFlags(rootCmd.Flags())
-}
-
-func setGlobalCliOptions() {
- // setup global CLI options (available on all CLI commands)
- rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
-
- flag := "quiet"
- rootCmd.PersistentFlags().BoolP(
- flag, "q", false,
- "suppress all logging output",
- )
- if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil {
- fmt.Printf("unable to bind flag '%s': %+v", flag, err)
- os.Exit(1)
- }
-
- rootCmd.PersistentFlags().CountVarP(&persistentOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
-}
-
-func setRootFlags(flags *pflag.FlagSet) {
- flags.StringP(
- "scope", "s", source.SquashedScope.String(),
- fmt.Sprintf("selection of layers to analyze, options=%v", source.AllScopes),
- )
-
- flags.StringArrayP(
- "output", "o", nil,
- fmt.Sprintf("report output formatter, formats=%v, deprecated formats=%v", format.AvailableFormats, format.DeprecatedFormats),
- )
-
- flags.StringP(
- "file", "", "",
- "file to write the default report output to (default is STDOUT)",
- )
-
- flags.StringP(
- "name", "", "",
- "set the name of the target being analyzed",
- )
-
- flags.StringP(
- "distro", "", "",
- "distro to match against in the format: :",
- )
-
- flags.BoolP(
- "add-cpes-if-none", "", false,
- "generate CPEs for packages with no CPE data",
- )
-
- flags.StringP("template", "t", "", "specify the path to a Go template file ("+
- "requires 'template' output to be selected)")
-
- flags.StringP(
- "fail-on", "f", "",
- fmt.Sprintf("set the return code to 1 if a vulnerability is found with a severity >= the given severity, options=%v", vulnerability.AllSeverities()),
- )
-
- flags.BoolP(
- "only-fixed", "", false,
- "ignore matches for vulnerabilities that are not fixed",
- )
-
- flags.BoolP(
- "only-notfixed", "", false,
- "ignore matches for vulnerabilities that are fixed",
- )
-
- flags.BoolP(
- "by-cve", "", false,
- "orient results by CVE instead of the original vulnerability ID when possible",
- )
-
- flags.BoolP(
- "show-suppressed", "", false,
- "show suppressed/ignored vulnerabilities in the output (only supported with table output format)",
- )
-
- flags.StringArrayP(
- "exclude", "", nil,
- "exclude paths from being scanned using a glob expression",
- )
-
- flags.StringP(
- "platform", "", "",
- "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')",
- )
-}
-
-//nolint:revive
-func bindRootConfigOptions(flags *pflag.FlagSet) error {
- if err := viper.BindPFlag("search.scope", flags.Lookup("scope")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("distro", flags.Lookup("distro")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("add-cpes-if-none", flags.Lookup("add-cpes-if-none")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("output-template-file", flags.Lookup("template")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("fail-on-severity", flags.Lookup("fail-on")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("only-fixed", flags.Lookup("only-fixed")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("only-notfixed", flags.Lookup("only-notfixed")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("by-cve", flags.Lookup("by-cve")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("show-suppressed", flags.Lookup("show-suppressed")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("platform", flags.Lookup("platform")); err != nil {
- return err
- }
-
- if err := viper.BindPFlag("name", flags.Lookup("name")); err != nil {
- return err
- }
-
- return nil
-}
-
-func rootExec(_ *cobra.Command, args []string) error {
- // we may not be provided an image if the user is piping in SBOM input
- var userInput string
- if len(args) > 0 {
- userInput = args[0]
- }
-
- return eventLoop(
- startWorker(userInput, appConfig.FailOnSeverity),
- setupSignals(),
- eventSubscription,
- stereoscope.Cleanup,
- ui.Select(isVerbose(), appConfig.Quiet)...,
- )
-}
-
-func isVerbose() (result bool) {
- isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect()
- if err != nil {
- // since we can't tell if there was piped input we assume that there could be to disable the ETUI
- log.Warnf("unable to determine if there is piped input: %+v", err)
- return true
- }
- // verbosity should consider if there is piped input (in which case we should not show the ETUI)
- return appConfig.CliOptions.Verbosity > 0 || isStdinPipeOrRedirect
-}
-
-//nolint:funlen
-func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-chan error {
- errs := make(chan error)
- go func() {
- defer close(errs)
- defer bus.Exit()
-
- writer, err := format.MakeScanResultWriter(appConfig.Outputs, appConfig.File, format.PresentationConfig{
- TemplateFilePath: appConfig.OutputTemplateFile,
- ShowSuppressed: appConfig.ShowSuppressed,
- })
- if err != nil {
- errs <- err
- return
- }
-
- checkForAppUpdate()
-
- var str *store.Store
- var status *db.Status
- var dbCloser *db.Closer
- var packages []pkg.Package
- var sbom *sbom.SBOM
- var pkgContext pkg.Context
- var wg = &sync.WaitGroup{}
- var loadedDB, gatheredPackages bool
-
- wg.Add(2)
-
- go func() {
- defer wg.Done()
- log.Debug("loading DB")
- str, status, dbCloser, err = grype.LoadVulnerabilityDB(appConfig.DB.ToCuratorConfig(), appConfig.DB.AutoUpdate)
- if err = validateDBLoad(err, status); err != nil {
- errs <- err
- return
- }
- loadedDB = true
- }()
-
- go func() {
- defer wg.Done()
- log.Debugf("gathering packages")
- // packages are grype.Package, not syft.Package
- // the SBOM is returned for downstream formatting concerns
- // grype uses the SBOM in combination with syft formatters to produce cycloneDX
- // with vulnerability information appended
- packages, pkgContext, sbom, err = pkg.Provide(userInput, getProviderConfig())
- if err != nil {
- errs <- fmt.Errorf("failed to catalog: %w", err)
- return
- }
- gatheredPackages = true
- }()
-
- wg.Wait()
- if !loadedDB || !gatheredPackages {
- return
- }
-
- if dbCloser != nil {
- defer dbCloser.Close()
- }
-
- if appConfig.OnlyFixed {
- appConfig.Ignore = append(appConfig.Ignore, ignoreNonFixedMatches...)
- }
-
- if appConfig.OnlyNotFixed {
- appConfig.Ignore = append(appConfig.Ignore, ignoreFixedMatches...)
- }
-
- applyDistroHint(packages, &pkgContext, appConfig)
-
- vulnMatcher := grype.VulnerabilityMatcher{
- Store: *str,
- IgnoreRules: appConfig.Ignore,
- NormalizeByCVE: appConfig.ByCVE,
- FailSeverity: failOnSeverity,
- Matchers: getMatchers(),
- }
-
- remainingMatches, ignoredMatches, err := vulnMatcher.FindMatches(packages, pkgContext)
- if err != nil {
- errs <- err
- if !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {
- return
- }
- }
-
- if err := writer.Write(models.PresenterConfig{
- Matches: *remainingMatches,
- IgnoredMatches: ignoredMatches,
- Packages: packages,
- Context: pkgContext,
- MetadataProvider: str,
- SBOM: sbom,
- AppConfig: appConfig,
- DBStatus: status,
- }); err != nil {
- errs <- err
- }
- }()
- return errs
-}
-
-func applyDistroHint(pkgs []pkg.Package, context *pkg.Context, appConfig *config.Application) {
- if appConfig.Distro != "" {
- log.Infof("using distro: %s", appConfig.Distro)
-
- split := strings.Split(appConfig.Distro, ":")
- d := split[0]
- v := ""
- if len(split) > 1 {
- v = split[1]
- }
- context.Distro = &linux.Release{
- PrettyName: d,
- Name: d,
- ID: d,
- IDLike: []string{
- d,
- },
- Version: v,
- VersionID: v,
- }
- }
-
- hasOSPackage := false
- for _, p := range pkgs {
- switch p.Type {
- case syftPkg.AlpmPkg, syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.KbPkg:
- hasOSPackage = true
- }
- }
-
- if context.Distro == nil && hasOSPackage {
- log.Warnf("Unable to determine the OS distribution. This may result in missing vulnerabilities. " +
- "You may specify a distro using: --distro :")
- }
-}
-
-func checkForAppUpdate() {
- if !appConfig.CheckForAppUpdate {
- return
- }
-
- isAvailable, newVersion, err := version.IsUpdateAvailable()
- if err != nil {
- log.Errorf(err.Error())
- }
- if isAvailable {
- log.Infof("new version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
-
- bus.Publish(partybus.Event{
- Type: event.CLIAppUpdateAvailable,
- Value: newVersion,
- })
- } else {
- log.Debugf("no new %s update available", internal.ApplicationName)
- }
-}
-
-func getMatchers() []matcher.Matcher {
- return matcher.NewDefaultMatchers(
- matcher.Config{
- Java: java.MatcherConfig{
- ExternalSearchConfig: appConfig.ExternalSources.ToJavaMatcherConfig(),
- UseCPEs: appConfig.Match.Java.UseCPEs,
- },
- Ruby: ruby.MatcherConfig(appConfig.Match.Ruby),
- Python: python.MatcherConfig(appConfig.Match.Python),
- Dotnet: dotnet.MatcherConfig(appConfig.Match.Dotnet),
- Javascript: javascript.MatcherConfig(appConfig.Match.Javascript),
- Golang: golang.MatcherConfig(appConfig.Match.Golang),
- Stock: stock.MatcherConfig(appConfig.Match.Stock),
- },
- )
-}
-
-func getProviderConfig() pkg.ProviderConfig {
- return pkg.ProviderConfig{
- SyftProviderConfig: pkg.SyftProviderConfig{
- RegistryOptions: appConfig.Registry.ToOptions(),
- Exclusions: appConfig.Exclusions,
- CatalogingOptions: appConfig.Search.ToConfig(),
- Platform: appConfig.Platform,
- Name: appConfig.Name,
- DefaultImagePullSource: appConfig.DefaultImagePullSource,
- },
- SynthesisConfig: pkg.SynthesisConfig{
- GenerateMissingCPEs: appConfig.GenerateMissingCPEs,
- },
- }
-}
-
-func validateDBLoad(loadErr error, status *db.Status) error {
- if loadErr != nil {
- return fmt.Errorf("failed to load vulnerability db: %w", loadErr)
- }
- 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)
- }
- return nil
-}
-
-func validateRootArgs(cmd *cobra.Command, args []string) error {
- isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect()
- if err != nil {
- log.Warnf("unable to determine if there is piped input: %+v", err)
- isStdinPipeOrRedirect = false
- }
-
- if len(args) == 0 && !isStdinPipeOrRedirect {
- // in the case that no arguments are given and there is no piped input we want to show the help text and return with a non-0 return code.
- if err := cmd.Help(); err != nil {
- return fmt.Errorf("unable to display help: %w", err)
- }
- return fmt.Errorf("an image/directory argument is required")
- }
-
- return cobra.MaximumNArgs(1)(cmd, args)
-}
diff --git a/cmd/grype/cli/legacy/root_test.go b/cmd/grype/cli/legacy/root_test.go
deleted file mode 100644
index 75b0433fa72..00000000000
--- a/cmd/grype/cli/legacy/root_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package legacy
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/internal/config"
-)
-
-func Test_applyDistroHint(t *testing.T) {
- ctx := pkg.Context{}
- cfg := config.Application{}
-
- applyDistroHint([]pkg.Package{}, &ctx, &cfg)
- assert.Nil(t, ctx.Distro)
-
- // works when distro is nil
- cfg.Distro = "alpine:3.10"
- applyDistroHint([]pkg.Package{}, &ctx, &cfg)
- assert.NotNil(t, ctx.Distro)
-
- assert.Equal(t, "alpine", ctx.Distro.Name)
- assert.Equal(t, "3.10", ctx.Distro.Version)
-
- // does override an existing distro
- cfg.Distro = "ubuntu:latest"
- applyDistroHint([]pkg.Package{}, &ctx, &cfg)
- assert.NotNil(t, ctx.Distro)
-
- assert.Equal(t, "ubuntu", ctx.Distro.Name)
- assert.Equal(t, "latest", 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)
-}
diff --git a/cmd/grype/cli/legacy/signals.go b/cmd/grype/cli/legacy/signals.go
deleted file mode 100644
index e70133231c8..00000000000
--- a/cmd/grype/cli/legacy/signals.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package legacy
-
-import (
- "os"
- "os/signal"
- "syscall"
-)
-
-func setupSignals() <-chan os.Signal {
- c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify
-
- interruptions := []os.Signal{
- syscall.SIGINT,
- syscall.SIGTERM,
- }
-
- signal.Notify(c, interruptions...)
-
- return c
-}
diff --git a/cmd/grype/cli/legacy/util.go b/cmd/grype/cli/legacy/util.go
deleted file mode 100644
index da07f95a858..00000000000
--- a/cmd/grype/cli/legacy/util.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package legacy
-
-import (
- "fmt"
- "os"
- "strings"
-)
-
-func stderrPrintLnf(message string, args ...interface{}) error {
- if !strings.HasSuffix(message, "\n") {
- message += "\n"
- }
- _, err := fmt.Fprintf(os.Stderr, message, args...)
- return err
-}
diff --git a/cmd/grype/cli/legacy/version.go b/cmd/grype/cli/legacy/version.go
deleted file mode 100644
index ce58d1618a0..00000000000
--- a/cmd/grype/cli/legacy/version.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package legacy
-
-import (
- "encoding/json"
- "fmt"
- "os"
-
- "github.com/spf13/cobra"
-
- "github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/grype/internal"
- "github.com/anchore/grype/internal/version"
-)
-
-var versionOutputFormat string
-
-var versionCmd = &cobra.Command{
- Use: "version",
- Short: "show the version",
- RunE: printVersion,
-}
-
-func init() {
- versionCmd.Flags().StringVarP(&versionOutputFormat, "output", "o", "text", "format to display results (available=[text, json])")
-
- rootCmd.AddCommand(versionCmd)
-}
-
-func printVersion(_ *cobra.Command, _ []string) error {
- versionInfo := version.FromBuild()
- switch versionOutputFormat {
- case "text":
- fmt.Println("Application: ", internal.ApplicationName)
- fmt.Println("Version: ", versionInfo.Version)
- fmt.Println("Syft Version: ", versionInfo.SyftVersion)
- fmt.Println("BuildDate: ", versionInfo.BuildDate)
- fmt.Println("GitCommit: ", versionInfo.GitCommit)
- fmt.Println("GitDescription: ", versionInfo.GitDescription)
- fmt.Println("Platform: ", versionInfo.Platform)
- fmt.Println("GoVersion: ", versionInfo.GoVersion)
- fmt.Println("Compiler: ", versionInfo.Compiler)
- fmt.Println("Supported DB Schema: ", vulnerability.SchemaVersion)
- case "json":
-
- enc := json.NewEncoder(os.Stdout)
- enc.SetEscapeHTML(false)
- enc.SetIndent("", " ")
- err := enc.Encode(&struct {
- version.Version
- Application string `json:"application"`
- SchemaVersion int `json:"supportedDbSchema"`
- }{
- Version: versionInfo,
- Application: internal.ApplicationName,
- SchemaVersion: vulnerability.SchemaVersion,
- })
- if err != nil {
- return fmt.Errorf("failed to show version information: %+v", err)
- }
- default:
- return fmt.Errorf("unsupported output format: %s", versionOutputFormat)
- }
- return nil
-}
diff --git a/cmd/grype/cli/options/database.go b/cmd/grype/cli/options/database.go
new file mode 100644
index 00000000000..ccafe815920
--- /dev/null
+++ b/cmd/grype/cli/options/database.go
@@ -0,0 +1,74 @@
+package options
+
+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"
+)
+
+type Database struct {
+ ID clio.Identification `yaml:"-" json:"-" mapstructure:"-"`
+ Dir string `yaml:"cache-dir" json:"cache-dir" mapstructure:"cache-dir"`
+ UpdateURL string `yaml:"update-url" json:"update-url" mapstructure:"update-url"`
+ CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"`
+ AutoUpdate bool `yaml:"auto-update" json:"auto-update" mapstructure:"auto-update"`
+ ValidateByHashOnStart bool `yaml:"validate-by-hash-on-start" json:"validate-by-hash-on-start" mapstructure:"validate-by-hash-on-start"`
+ ValidateAge bool `yaml:"validate-age" json:"validate-age" mapstructure:"validate-age"`
+ MaxAllowedBuiltAge time.Duration `yaml:"max-allowed-built-age" json:"max-allowed-built-age" mapstructure:"max-allowed-built-age"`
+ RequireUpdateCheck bool `yaml:"require-update-check" json:"require-update-check" mapstructure:"require-update-check"`
+ UpdateAvailableTimeout time.Duration `yaml:"update-available-timeout" json:"update-available-timeout" mapstructure:"update-available-timeout"`
+ UpdateDownloadTimeout time.Duration `yaml:"update-download-timeout" json:"update-download-timeout" mapstructure:"update-download-timeout"`
+ MaxUpdateCheckFrequency time.Duration `yaml:"max-update-check-frequency" json:"max-update-check-frequency" mapstructure:"max-update-check-frequency"`
+}
+
+var _ interface {
+ clio.FieldDescriber
+ clio.PostLoader
+} = (*Database)(nil)
+
+func DefaultDatabase(id clio.Identification) Database {
+ distConfig := distribution.DefaultConfig()
+ installConfig := installation.DefaultConfig(id)
+ return Database{
+ ID: id,
+ Dir: installConfig.DBRootDir,
+ UpdateURL: distConfig.LatestURL,
+ AutoUpdate: true,
+ ValidateAge: installConfig.ValidateAge,
+ // After this period (5 days) the db data is considered stale
+ MaxAllowedBuiltAge: installConfig.MaxAllowedBuiltAge,
+ RequireUpdateCheck: distConfig.RequireUpdateCheck,
+ ValidateByHashOnStart: installConfig.ValidateChecksum,
+ UpdateAvailableTimeout: distConfig.CheckTimeout,
+ UpdateDownloadTimeout: distConfig.UpdateTimeout,
+ MaxUpdateCheckFrequency: installConfig.UpdateCheckMaxFrequency,
+ CACert: distConfig.CACert,
+ }
+}
+
+func (cfg *Database) DescribeFields(descriptions clio.FieldDescriptionSet) {
+ descriptions.Add(&cfg.Dir, `location to write the vulnerability database cache`)
+ descriptions.Add(&cfg.UpdateURL, `URL of the vulnerability database`)
+ descriptions.Add(&cfg.CACert, `certificate to trust download the database and listing file`)
+ descriptions.Add(&cfg.AutoUpdate, `check for database updates on execution`)
+ descriptions.Add(&cfg.ValidateAge, `ensure db build is no older than the max-allowed-built-age`)
+ descriptions.Add(&cfg.ValidateByHashOnStart, `validate the database matches the known hash each execution`)
+ descriptions.Add(&cfg.MaxAllowedBuiltAge, `Max allowed age for vulnerability database,
+age being the time since it was built
+Default max age is 120h (or five days)`)
+ descriptions.Add(&cfg.RequireUpdateCheck, `fail the scan if unable to check for database updates`)
+ descriptions.Add(&cfg.UpdateAvailableTimeout, `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`)
+ descriptions.Add(&cfg.UpdateDownloadTimeout, `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`)
+ 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_command.go b/cmd/grype/cli/options/database_command.go
new file mode 100644
index 00000000000..50842c186a7
--- /dev/null
+++ b/cmd/grype/cli/options/database_command.go
@@ -0,0 +1,46 @@
+package options
+
+import (
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ "github.com/anchore/grype/grype/db/v6/installation"
+)
+
+type DatabaseCommand struct {
+ DB Database `yaml:"db" json:"db" mapstructure:"db"`
+ Experimental Experimental `yaml:"exp" json:"exp" mapstructure:"exp"`
+ Developer developer `yaml:"dev" json:"dev" mapstructure:"dev"`
+}
+
+func DefaultDatabaseCommand(id clio.Identification) *DatabaseCommand {
+ dbDefaults := DefaultDatabase(id)
+ // by default, require update check success for db operations which check for updates
+ dbDefaults.RequireUpdateCheck = true
+ // we want to validate by hash during Status checks
+ dbDefaults.ValidateByHashOnStart = true
+ return &DatabaseCommand{
+ DB: dbDefaults,
+ }
+}
+
+func (cfg DatabaseCommand) ToCuratorConfig() installation.Config {
+ return installation.Config{
+ DBRootDir: cfg.DB.Dir,
+ ValidateAge: cfg.DB.ValidateAge,
+ ValidateChecksum: cfg.DB.ValidateByHashOnStart,
+ MaxAllowedBuiltAge: cfg.DB.MaxAllowedBuiltAge,
+ UpdateCheckMaxFrequency: cfg.DB.MaxUpdateCheckFrequency,
+ Debug: cfg.Developer.DB.Debug,
+ }
+}
+
+func (cfg DatabaseCommand) ToClientConfig() distribution.Config {
+ return distribution.Config{
+ ID: cfg.DB.ID,
+ LatestURL: cfg.DB.UpdateURL,
+ CACert: cfg.DB.CACert,
+ RequireUpdateCheck: cfg.DB.RequireUpdateCheck,
+ CheckTimeout: cfg.DB.UpdateAvailableTimeout,
+ UpdateTimeout: cfg.DB.UpdateDownloadTimeout,
+ }
+}
diff --git a/cmd/grype/cli/options/database_search_bounds.go b/cmd/grype/cli/options/database_search_bounds.go
new file mode 100644
index 00000000000..56a62322498
--- /dev/null
+++ b/cmd/grype/cli/options/database_search_bounds.go
@@ -0,0 +1,29 @@
+package options
+
+import (
+ "fmt"
+
+ "github.com/anchore/clio"
+)
+
+type DBSearchBounds struct {
+ RecordLimit int `yaml:"limit" json:"limit" mapstructure:"limit"`
+}
+
+func DefaultDBSearchBounds() DBSearchBounds {
+ return DBSearchBounds{
+ RecordLimit: 5000,
+ }
+}
+
+func (o *DBSearchBounds) AddFlags(flags clio.FlagSet) {
+ flags.IntVarP(&o.RecordLimit, "limit", "", "limit the number of results returned, use 0 for no limit")
+}
+
+func (o *DBSearchBounds) PostLoad() error {
+ if o.RecordLimit < 0 {
+ return fmt.Errorf("limit must be a positive integer")
+ }
+
+ return nil
+}
diff --git a/cmd/grype/cli/options/database_search_format.go b/cmd/grype/cli/options/database_search_format.go
new file mode 100644
index 00000000000..0ea068f5e78
--- /dev/null
+++ b/cmd/grype/cli/options/database_search_format.go
@@ -0,0 +1,36 @@
+package options
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/scylladb/go-set/strset"
+
+ "github.com/anchore/clio"
+)
+
+type DBSearchFormat struct {
+ Output string `yaml:"output" json:"output" mapstructure:"output"`
+ Allowable []string `yaml:"-" json:"-" mapstructure:"-"`
+}
+
+func DefaultDBSearchFormat() DBSearchFormat {
+ return DBSearchFormat{
+ Output: "table",
+ Allowable: []string{"table", "json"},
+ }
+}
+
+func (c *DBSearchFormat) AddFlags(flags clio.FlagSet) {
+ available := strings.Join(c.Allowable, ", ")
+ flags.StringVarP(&c.Output, "output", "o", fmt.Sprintf("format to display results (available=[%s])", available))
+}
+
+func (c *DBSearchFormat) PostLoad() error {
+ if len(c.Allowable) > 0 {
+ if !strset.New(c.Allowable...).Has(c.Output) {
+ return fmt.Errorf("invalid output format: %s (expected one of: %s)", c.Output, strings.Join(c.Allowable, ", "))
+ }
+ }
+ return nil
+}
diff --git a/cmd/grype/cli/options/database_search_os.go b/cmd/grype/cli/options/database_search_os.go
new file mode 100644
index 00000000000..00c073dd881
--- /dev/null
+++ b/cmd/grype/cli/options/database_search_os.go
@@ -0,0 +1,95 @@
+package options
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "unicode"
+
+ "github.com/anchore/clio"
+ v6 "github.com/anchore/grype/grype/db/v6"
+)
+
+type DBSearchOSs struct {
+ OSs []string `yaml:"distro" json:"distro" mapstructure:"distro"`
+ Specs v6.OSSpecifiers `yaml:"-" json:"-" mapstructure:"-"`
+}
+
+func (o *DBSearchOSs) AddFlags(flags clio.FlagSet) {
+ // consistent with grype --distro flag today
+ flags.StringArrayVarP(&o.OSs, "distro", "", "refine to results with the given operating system (format: 'name', 'name@version', 'name@maj.min', 'name@codename')")
+}
+
+func (o *DBSearchOSs) PostLoad() error {
+ if len(o.OSs) == 0 {
+ o.Specs = []*v6.OSSpecifier{v6.AnyOSSpecified}
+ return nil
+ }
+
+ var specs []*v6.OSSpecifier
+ for _, osValue := range o.OSs {
+ spec, err := parseOSString(osValue)
+ if err != nil {
+ return err
+ }
+ specs = append(specs, spec)
+ }
+ o.Specs = specs
+
+ return nil
+}
+
+func parseOSString(osValue string) (*v6.OSSpecifier, error) {
+ // parse name@version from the distro string
+ // version could be a codename, major version, major.minor version, or major.minior.patch version
+ switch strings.Count(osValue, ":") {
+ case 0:
+ // no-op
+ case 1:
+ // be nice to folks that are close...
+ osValue = strings.ReplaceAll(osValue, ":", "@")
+ default:
+ // this is pretty unexpected
+ return nil, fmt.Errorf("invalid distro input provided: %q", osValue)
+ }
+
+ parts := strings.Split(osValue, "@")
+ switch len(parts) {
+ case 1:
+ name := strings.TrimSpace(parts[0])
+ return &v6.OSSpecifier{Name: name}, nil
+ case 2:
+ version := strings.TrimSpace(parts[1])
+ name := strings.TrimSpace(parts[0])
+ if len(version) == 0 {
+ return nil, errors.New("invalid distro version provided")
+ }
+
+ // parse the version (major.minor.patch, major.minor, major, codename)
+
+ // if starts with a number, then it is a version
+ if unicode.IsDigit(rune(version[0])) {
+ versionParts := strings.Split(parts[1], ".")
+ var major, minor string
+ switch len(versionParts) {
+ case 1:
+ major = versionParts[0]
+ case 2:
+ major = versionParts[0]
+ minor = versionParts[1]
+ case 3:
+ return nil, fmt.Errorf("invalid distro version provided: patch version ignored: %q", version)
+ default:
+ return nil, fmt.Errorf("invalid distro version provided: %q", version)
+ }
+
+ return &v6.OSSpecifier{Name: name, MajorVersion: major, MinorVersion: minor}, nil
+ }
+
+ // is codename / label
+ return &v6.OSSpecifier{Name: name, LabelVersion: version}, nil
+
+ default:
+ return nil, fmt.Errorf("invalid distro name@version: %q", osValue)
+ }
+}
diff --git a/cmd/grype/cli/options/database_search_os_test.go b/cmd/grype/cli/options/database_search_os_test.go
new file mode 100644
index 00000000000..26b485fc1dc
--- /dev/null
+++ b/cmd/grype/cli/options/database_search_os_test.go
@@ -0,0 +1,108 @@
+package options
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/require"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+)
+
+func TestDBSearchOSsPostLoad(t *testing.T) {
+ testCases := []struct {
+ name string
+ input DBSearchOSs
+ expectedSpecs v6.OSSpecifiers
+ expectedErrMsg string
+ }{
+ {
+ name: "no OS input (any OS)",
+ input: DBSearchOSs{},
+ expectedSpecs: []*v6.OSSpecifier{v6.AnyOSSpecified},
+ },
+ {
+ name: "valid OS name only",
+ input: DBSearchOSs{
+ OSs: []string{"ubuntu"},
+ },
+ expectedSpecs: []*v6.OSSpecifier{
+ {Name: "ubuntu"},
+ },
+ },
+ {
+ name: "valid OS with major version",
+ input: DBSearchOSs{
+ OSs: []string{"ubuntu@20"},
+ },
+ expectedSpecs: []*v6.OSSpecifier{
+ {Name: "ubuntu", MajorVersion: "20"},
+ },
+ },
+ {
+ name: "valid OS with major and minor version",
+ input: DBSearchOSs{
+ OSs: []string{"ubuntu@20.04"},
+ },
+ expectedSpecs: []*v6.OSSpecifier{
+ {Name: "ubuntu", MajorVersion: "20", MinorVersion: "04"},
+ },
+ },
+ {
+ name: "valid OS with codename",
+ input: DBSearchOSs{
+ OSs: []string{"ubuntu@focal"},
+ },
+ expectedSpecs: []*v6.OSSpecifier{
+ {Name: "ubuntu", LabelVersion: "focal"},
+ },
+ },
+ {
+ name: "invalid OS version (too many parts)",
+ input: DBSearchOSs{
+ OSs: []string{"ubuntu@20.04.1"},
+ },
+ expectedErrMsg: "invalid distro version provided: patch version ignored",
+ },
+ {
+ name: "invalid OS format with colon",
+ input: DBSearchOSs{
+ OSs: []string{"ubuntu:20"},
+ },
+ expectedSpecs: []*v6.OSSpecifier{
+ {Name: "ubuntu", MajorVersion: "20"},
+ },
+ },
+ {
+ name: "invalid OS with empty version",
+ input: DBSearchOSs{
+ OSs: []string{"ubuntu@"},
+ },
+ expectedErrMsg: "invalid distro version provided",
+ },
+ {
+ name: "invalid OS name@version format",
+ input: DBSearchOSs{
+ OSs: []string{"ubuntu@20@04"},
+ },
+ expectedErrMsg: "invalid distro name@version",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := tc.input.PostLoad()
+
+ if tc.expectedErrMsg != "" {
+ require.Error(t, err)
+ require.ErrorContains(t, err, tc.expectedErrMsg)
+ return
+ }
+
+ require.NoError(t, err)
+ if d := cmp.Diff(tc.expectedSpecs, tc.input.Specs); d != "" {
+ t.Errorf("unexpected OS specifiers (-want +got):\n%s", d)
+ }
+ })
+ }
+}
diff --git a/cmd/grype/cli/options/database_search_packages.go b/cmd/grype/cli/options/database_search_packages.go
new file mode 100644
index 00000000000..68e7a6679b6
--- /dev/null
+++ b/cmd/grype/cli/options/database_search_packages.go
@@ -0,0 +1,84 @@
+package options
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/anchore/clio"
+ v6 "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/packageurl-go"
+ "github.com/anchore/syft/syft/cpe"
+)
+
+type DBSearchPackages struct {
+ AllowBroadCPEMatching bool `yaml:"allow-broad-cpe-matching" json:"allow-broad-cpe-matching" mapstructure:"allow-broad-cpe-matching"`
+ Packages []string `yaml:"packages" json:"packages" mapstructure:"packages"`
+ Ecosystem string `yaml:"ecosystem" json:"ecosystem" mapstructure:"ecosystem"`
+ PkgSpecs v6.PackageSpecifiers `yaml:"-" json:"-" mapstructure:"-"`
+ CPESpecs v6.PackageSpecifiers `yaml:"-" json:"-" mapstructure:"-"`
+}
+
+func (o *DBSearchPackages) AddFlags(flags clio.FlagSet) {
+ flags.StringArrayVarP(&o.Packages, "pkg", "", "package name/CPE/PURL to search for")
+ flags.StringVarP(&o.Ecosystem, "ecosystem", "", "ecosystem of the package to search within")
+ flags.BoolVarP(&o.AllowBroadCPEMatching, "broad-cpe-matching", "", "allow for specific package CPE attributes to match with '*' values on the vulnerability")
+}
+
+func (o *DBSearchPackages) PostLoad() error {
+ // note: this may be called multiple times, so we need to reset the specs each time
+ o.PkgSpecs = nil
+ o.CPESpecs = nil
+
+ for _, p := range o.Packages {
+ switch {
+ case strings.HasPrefix(p, "cpe:"):
+ c, err := cpe.NewAttributes(p)
+ if err != nil {
+ return fmt.Errorf("invalid CPE from %q: %w", o.Packages, err)
+ }
+
+ if c.Version != "" || c.Update != "" {
+ log.Warnf("ignoring version and update values for %q", p)
+ c.Version = ""
+ c.Update = ""
+ }
+
+ s := &v6.PackageSpecifier{CPE: &c}
+ o.CPESpecs = append(o.CPESpecs, s)
+ o.PkgSpecs = append(o.PkgSpecs, s)
+ case strings.HasPrefix(p, "pkg:"):
+ if o.Ecosystem != "" {
+ return errors.New("cannot specify both package URL and ecosystem")
+ }
+
+ purl, err := packageurl.FromString(p)
+ if err != nil {
+ return fmt.Errorf("invalid package URL from %q: %w", o.Packages, err)
+ }
+
+ if purl.Version != "" || len(purl.Qualifiers) > 0 {
+ log.Warnf("ignoring version and qualifiers for package URL %q", purl)
+ }
+
+ o.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Name: purl.Name, Ecosystem: purl.Type})
+ o.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: "a", Product: purl.Name, TargetSW: purl.Type}})
+
+ default:
+ o.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Name: p, Ecosystem: o.Ecosystem})
+ o.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{
+ CPE: &cpe.Attributes{Part: "a", Product: p},
+ })
+ }
+ }
+
+ if len(o.Packages) == 0 {
+ if o.Ecosystem != "" {
+ o.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Ecosystem: o.Ecosystem})
+ o.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{CPE: &cpe.Attributes{TargetSW: o.Ecosystem}})
+ }
+ }
+
+ return nil
+}
diff --git a/cmd/grype/cli/options/database_search_packages_test.go b/cmd/grype/cli/options/database_search_packages_test.go
new file mode 100644
index 00000000000..a806dc4213c
--- /dev/null
+++ b/cmd/grype/cli/options/database_search_packages_test.go
@@ -0,0 +1,112 @@
+package options
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/require"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/syft/syft/cpe"
+)
+
+func TestDBSearchPackagesPostLoad(t *testing.T) {
+ testCases := []struct {
+ name string
+ input DBSearchPackages
+ expectedPkg v6.PackageSpecifiers
+ expectedCPE v6.PackageSpecifiers
+ expectedErrMsg string
+ }{
+ {
+ name: "valid CPE",
+ input: DBSearchPackages{
+ Packages: []string{"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*"},
+ },
+ expectedPkg: v6.PackageSpecifiers{
+ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor", Product: "product"}},
+ },
+ expectedCPE: v6.PackageSpecifiers{
+ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor", Product: "product"}},
+ },
+ },
+ {
+ name: "valid PURL",
+ input: DBSearchPackages{
+ Packages: []string{"pkg:npm/package-name@1.0.0"},
+ },
+ expectedPkg: v6.PackageSpecifiers{
+ {Name: "package-name", Ecosystem: "npm"},
+ },
+ expectedCPE: v6.PackageSpecifiers{
+ {CPE: &cpe.Attributes{Part: "a", Product: "package-name", TargetSW: "npm"}},
+ },
+ },
+ {
+ name: "plain package name",
+ input: DBSearchPackages{
+ Packages: []string{"package-name"},
+ },
+ expectedPkg: v6.PackageSpecifiers{
+ {Name: "package-name"},
+ },
+ expectedCPE: v6.PackageSpecifiers{
+ {CPE: &cpe.Attributes{Part: "a", Product: "package-name"}},
+ },
+ },
+ {
+ name: "ecosystem without packages",
+ input: DBSearchPackages{
+ Ecosystem: "npm",
+ },
+ expectedPkg: v6.PackageSpecifiers{
+ {Ecosystem: "npm"},
+ },
+ expectedCPE: v6.PackageSpecifiers{
+ {CPE: &cpe.Attributes{TargetSW: "npm"}},
+ },
+ },
+ {
+ name: "conflicting PURL and ecosystem",
+ input: DBSearchPackages{
+ Packages: []string{"pkg:npm/package-name@1.0.0"},
+ Ecosystem: "npm",
+ },
+ expectedErrMsg: "cannot specify both package URL and ecosystem",
+ },
+ {
+ name: "invalid CPE",
+ input: DBSearchPackages{
+ Packages: []string{"cpe:2.3:a:$%&^*%"},
+ },
+ expectedErrMsg: "invalid CPE",
+ },
+ {
+ name: "invalid PURL",
+ input: DBSearchPackages{
+ Packages: []string{"pkg:invalid"},
+ },
+ expectedErrMsg: "invalid package URL",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := tc.input.PostLoad()
+
+ if tc.expectedErrMsg != "" {
+ require.Error(t, err)
+ require.ErrorContains(t, err, tc.expectedErrMsg)
+ return
+ }
+ require.NoError(t, err)
+ if d := cmp.Diff(tc.expectedPkg, tc.input.PkgSpecs); d != "" {
+ t.Errorf("unexpected package specifiers (-want +got):\n%s", d)
+ }
+ if d := cmp.Diff(tc.expectedCPE, tc.input.CPESpecs); d != "" {
+ t.Errorf("unexpected CPE specifiers (-want +got):\n%s", d)
+ }
+
+ })
+ }
+}
diff --git a/cmd/grype/cli/options/database_search_vulnerabilities.go b/cmd/grype/cli/options/database_search_vulnerabilities.go
new file mode 100644
index 00000000000..ff47253bda8
--- /dev/null
+++ b/cmd/grype/cli/options/database_search_vulnerabilities.go
@@ -0,0 +1,87 @@
+package options
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/araddon/dateparse"
+
+ "github.com/anchore/clio"
+ v6 "github.com/anchore/grype/grype/db/v6"
+)
+
+type DBSearchVulnerabilities struct {
+ VulnerabilityIDs []string `yaml:"vulnerability-ids" json:"vulnerability-ids" mapstructure:"vulnerability-ids"`
+ UseVulnIDFlag bool `yaml:"-" json:"-" mapstructure:"-"`
+
+ PublishedAfter string `yaml:"published-after" json:"published-after" mapstructure:"published-after"`
+ ModifiedAfter string `yaml:"modified-after" json:"modified-after" mapstructure:"modified-after"`
+
+ Providers []string `yaml:"providers" json:"providers" mapstructure:"providers"`
+
+ Specs v6.VulnerabilitySpecifiers `yaml:"-" json:"-" mapstructure:"-"`
+}
+
+func (c *DBSearchVulnerabilities) AddFlags(flags clio.FlagSet) {
+ if c.UseVulnIDFlag {
+ flags.StringArrayVarP(&c.VulnerabilityIDs, "vuln", "", "only show results for the given vulnerability ID")
+ }
+ flags.StringVarP(&c.PublishedAfter, "published-after", "", "only show vulnerabilities originally published after the given date (format: YYYY-MM-DD)")
+ flags.StringVarP(&c.ModifiedAfter, "modified-after", "", "only show vulnerabilities originally published or modified since the given date (format: YYYY-MM-DD)")
+ flags.StringArrayVarP(&c.Providers, "provider", "", "only show vulnerabilities from the given provider")
+}
+
+func (c *DBSearchVulnerabilities) PostLoad() error {
+ // note: this may be called multiple times, so we need to reset the specs each time
+ c.Specs = nil
+
+ handleTimeOption := func(val string, flag string) (*time.Time, error) {
+ if val == "" {
+ return nil, nil
+ }
+ parsed, err := dateparse.ParseIn(val, time.UTC)
+ if err != nil {
+ return nil, fmt.Errorf("invalid date format for %s=%q: %w", flag, val, err)
+ }
+ return &parsed, nil
+ }
+
+ if c.PublishedAfter != "" && c.ModifiedAfter != "" {
+ return fmt.Errorf("only one of --published-after or --modified-after can be set")
+ }
+
+ var publishedAfter, modifiedAfter *time.Time
+ var err error
+ publishedAfter, err = handleTimeOption(c.PublishedAfter, "published-after")
+ if err != nil {
+ return fmt.Errorf("invalid date format for published-after field: %w", err)
+ }
+ modifiedAfter, err = handleTimeOption(c.ModifiedAfter, "modified-after")
+ if err != nil {
+ return fmt.Errorf("invalid date format for modified-after field: %w", err)
+ }
+
+ var specs []v6.VulnerabilitySpecifier
+ for _, vulnID := range c.VulnerabilityIDs {
+ specs = append(specs, v6.VulnerabilitySpecifier{
+ Name: vulnID,
+ PublishedAfter: publishedAfter,
+ ModifiedAfter: modifiedAfter,
+ Providers: c.Providers,
+ })
+ }
+
+ if len(specs) == 0 {
+ if c.PublishedAfter != "" || c.ModifiedAfter != "" || len(c.Providers) > 0 {
+ specs = append(specs, v6.VulnerabilitySpecifier{
+ PublishedAfter: publishedAfter,
+ ModifiedAfter: modifiedAfter,
+ Providers: c.Providers,
+ })
+ }
+ }
+
+ c.Specs = specs
+
+ return nil
+}
diff --git a/cmd/grype/cli/options/database_search_vulnerabilities_test.go b/cmd/grype/cli/options/database_search_vulnerabilities_test.go
new file mode 100644
index 00000000000..d63677748dc
--- /dev/null
+++ b/cmd/grype/cli/options/database_search_vulnerabilities_test.go
@@ -0,0 +1,120 @@
+package options
+
+import (
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/require"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+)
+
+func TestDBSearchVulnerabilitiesPostLoad(t *testing.T) {
+ testCases := []struct {
+ name string
+ input DBSearchVulnerabilities
+ expectedSpecs v6.VulnerabilitySpecifiers
+ expectedErrMsg string
+ }{
+ {
+ name: "single vulnerability ID",
+ input: DBSearchVulnerabilities{
+ VulnerabilityIDs: []string{"CVE-2023-0001"},
+ },
+ expectedSpecs: v6.VulnerabilitySpecifiers{
+ {Name: "CVE-2023-0001"},
+ },
+ },
+ {
+ name: "multiple vulnerability IDs",
+ input: DBSearchVulnerabilities{
+ VulnerabilityIDs: []string{"CVE-2023-0001", "GHSA-1234"},
+ },
+ expectedSpecs: v6.VulnerabilitySpecifiers{
+ {Name: "CVE-2023-0001"},
+ {Name: "GHSA-1234"},
+ },
+ },
+ {
+ name: "published-after set",
+ input: DBSearchVulnerabilities{
+ PublishedAfter: "2023-01-01",
+ },
+ expectedSpecs: v6.VulnerabilitySpecifiers{
+ {PublishedAfter: parseTime("2023-01-01")},
+ },
+ },
+ {
+ name: "modified-after set",
+ input: DBSearchVulnerabilities{
+ ModifiedAfter: "2023-02-01",
+ },
+ expectedSpecs: v6.VulnerabilitySpecifiers{
+ {ModifiedAfter: parseTime("2023-02-01")},
+ },
+ },
+ {
+ name: "both published-after and modified-after set",
+ input: DBSearchVulnerabilities{
+ PublishedAfter: "2023-01-01",
+ ModifiedAfter: "2023-02-01",
+ },
+ expectedErrMsg: "only one of --published-after or --modified-after can be set",
+ },
+ {
+ name: "invalid date for published-after",
+ input: DBSearchVulnerabilities{
+ PublishedAfter: "invalid-date",
+ },
+ expectedErrMsg: "invalid date format for published-after",
+ },
+ {
+ name: "invalid date for modified-after",
+ input: DBSearchVulnerabilities{
+ ModifiedAfter: "invalid-date",
+ },
+ expectedErrMsg: "invalid date format for modified-after",
+ },
+ {
+ name: "vulnerability ID with providers",
+ input: DBSearchVulnerabilities{
+ VulnerabilityIDs: []string{"CVE-2023-0001"},
+ Providers: []string{"provider1"},
+ },
+ expectedSpecs: v6.VulnerabilitySpecifiers{
+ {Name: "CVE-2023-0001", Providers: []string{"provider1"}},
+ },
+ },
+ {
+ name: "providers without vulnerability IDs",
+ input: DBSearchVulnerabilities{
+ Providers: []string{"provider1", "provider2"},
+ },
+ expectedSpecs: v6.VulnerabilitySpecifiers{
+ {Providers: []string{"provider1", "provider2"}},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := tc.input.PostLoad()
+
+ if tc.expectedErrMsg != "" {
+ require.Error(t, err)
+ require.ErrorContains(t, err, tc.expectedErrMsg)
+ return
+ }
+ require.NoError(t, err)
+ if d := cmp.Diff(tc.expectedSpecs, tc.input.Specs); d != "" {
+ t.Errorf("unexpected vulnerability specifiers (-want +got):\n%s", d)
+ }
+ })
+ }
+}
+
+func parseTime(value string) *time.Time {
+ t, _ := time.Parse("2006-01-02", value)
+ return &t
+}
diff --git a/cmd/grype/cli/options/datasources.go b/cmd/grype/cli/options/datasources.go
new file mode 100644
index 00000000000..c0f5c00d6d8
--- /dev/null
+++ b/cmd/grype/cli/options/datasources.go
@@ -0,0 +1,56 @@
+package options
+
+import (
+ "time"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/grype/matcher/java"
+)
+
+const (
+ defaultMavenBaseURL = "https://search.maven.org/solrsearch/select"
+)
+
+type externalSources struct {
+ Enable bool `yaml:"enable" json:"enable" mapstructure:"enable"`
+ Maven maven `yaml:"maven" json:"maven" mapstructure:"maven"`
+}
+
+var _ interface {
+ clio.FieldDescriber
+} = (*externalSources)(nil)
+
+type maven struct {
+ SearchUpstreamBySha1 bool `yaml:"search-upstream" json:"searchUpstreamBySha1" mapstructure:"search-maven-upstream"`
+ BaseURL string `yaml:"base-url" json:"baseUrl" mapstructure:"base-url"`
+ RateLimit time.Duration `yaml:"rate-limit" json:"rateLimit" mapstructure:"rate-limit"`
+}
+
+func defaultExternalSources() externalSources {
+ return externalSources{
+ Maven: maven{
+ SearchUpstreamBySha1: true,
+ BaseURL: defaultMavenBaseURL,
+ RateLimit: 300 * time.Millisecond,
+ },
+ }
+}
+
+func (cfg externalSources) ToJavaMatcherConfig() java.ExternalSearchConfig {
+ // always respect if global config is disabled
+ smu := cfg.Maven.SearchUpstreamBySha1
+ if !cfg.Enable {
+ smu = cfg.Enable
+ }
+ return java.ExternalSearchConfig{
+ SearchMavenUpstream: smu,
+ MavenBaseURL: cfg.Maven.BaseURL,
+ MavenRateLimit: cfg.Maven.RateLimit,
+ }
+}
+
+func (cfg *externalSources) DescribeFields(descriptions clio.FieldDescriptionSet) {
+ descriptions.Add(&cfg.Enable, `enable Grype searching network source for additional information`)
+ descriptions.Add(&cfg.Maven.SearchUpstreamBySha1, `search for Maven artifacts by SHA1`)
+ descriptions.Add(&cfg.Maven.BaseURL, `base URL of the Maven repository to search`)
+}
diff --git a/cmd/grype/cli/options/experimental.go b/cmd/grype/cli/options/experimental.go
new file mode 100644
index 00000000000..9666e7c9fa9
--- /dev/null
+++ b/cmd/grype/cli/options/experimental.go
@@ -0,0 +1,10 @@
+package options
+
+// Experimental options are opt-in features that are...
+// ...not stable
+// ...not yet fully supported
+// ...not necessarily tested
+// ...not ready for production use
+// these may go away at any moment, do not depend on them
+type Experimental struct {
+}
diff --git a/cmd/grype/cli/options/grype.go b/cmd/grype/cli/options/grype.go
new file mode 100644
index 00000000000..0dab835425b
--- /dev/null
+++ b/cmd/grype/cli/options/grype.go
@@ -0,0 +1,205 @@
+package options
+
+import (
+ "fmt"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/format"
+ "github.com/anchore/syft/syft/source"
+)
+
+type Grype struct {
+ Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, = the Presenter hint string to use for report formatting and the output file
+ File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
+ Pretty bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
+ Distro string `yaml:"distro" json:"distro" mapstructure:"distro"` // --distro, specify a distro to explicitly use
+ GenerateMissingCPEs bool `yaml:"add-cpes-if-none" json:"add-cpes-if-none" mapstructure:"add-cpes-if-none"` // --add-cpes-if-none, automatically generate CPEs if they are not present in import (e.g. from a 3rd party SPDX document)
+ OutputTemplateFile string `yaml:"output-template-file" json:"output-template-file" mapstructure:"output-template-file"` // -t, the template file to use for formatting the final report
+ CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
+ OnlyFixed bool `yaml:"only-fixed" json:"only-fixed" mapstructure:"only-fixed"` // only fail if detected vulns have a fix
+ OnlyNotFixed bool `yaml:"only-notfixed" json:"only-notfixed" mapstructure:"only-notfixed"` // only fail if detected vulns don't have a fix
+ IgnoreStates string `yaml:"ignore-states" json:"ignore-wontfix" mapstructure:"ignore-wontfix"` // ignore detections for vulnerabilities matching these comma-separated fix states
+ Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override the target platform for a container image
+ Search search `yaml:"search" json:"search" mapstructure:"search"`
+ Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"`
+ Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
+ ExternalSources externalSources `yaml:"external-sources" json:"externalSources" mapstructure:"external-sources"`
+ Match matchConfig `yaml:"match" json:"match" mapstructure:"match"`
+ FailOn string `yaml:"fail-on-severity" json:"fail-on-severity" mapstructure:"fail-on-severity"`
+ 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"`
+ VexAdd []string `yaml:"vex-add" json:"vex-add" mapstructure:"vex-add"` // GRYPE_VEX_ADD
+ MatchUpstreamKernelHeaders bool `yaml:"match-upstream-kernel-headers" json:"match-upstream-kernel-headers" mapstructure:"match-upstream-kernel-headers"` // Show matches on kernel-headers packages where the match is on kernel upstream instead of marking them as ignored, default=false
+ DatabaseCommand `yaml:",inline" json:",inline" mapstructure:",squash"`
+}
+
+type developer struct {
+ DB databaseDeveloper `yaml:"db" json:"db" mapstructure:"db"`
+}
+
+type databaseDeveloper struct {
+ Debug bool `yaml:"debug" json:"debug" mapstructure:"debug"`
+}
+
+var _ interface {
+ clio.FlagAdder
+ clio.PostLoader
+ clio.FieldDescriber
+} = (*Grype)(nil)
+
+func DefaultGrype(id clio.Identification) *Grype {
+ return &Grype{
+ Search: defaultSearch(source.SquashedScope),
+ DatabaseCommand: DatabaseCommand{
+ DB: DefaultDatabase(id),
+ },
+ Match: defaultMatchConfig(),
+ ExternalSources: defaultExternalSources(),
+ CheckForAppUpdate: true,
+ VexAdd: []string{},
+ MatchUpstreamKernelHeaders: false,
+ SortBy: defaultSortBy(),
+ }
+}
+
+// nolint:funlen
+func (o *Grype) AddFlags(flags clio.FlagSet) {
+ flags.StringVarP(&o.Search.Scope,
+ "scope", "s",
+ fmt.Sprintf("selection of layers to analyze, options=%v", source.AllScopes),
+ )
+
+ flags.StringArrayVarP(&o.Outputs,
+ "output", "o",
+ fmt.Sprintf("report output formatter, formats=%v, deprecated formats=%v", format.AvailableFormats, format.DeprecatedFormats),
+ )
+
+ flags.StringVarP(&o.File,
+ "file", "",
+ "file to write the default report output to (default is STDOUT)",
+ )
+
+ flags.StringVarP(&o.Name,
+ "name", "",
+ "set the name of the target being analyzed",
+ )
+
+ flags.StringVarP(&o.Distro,
+ "distro", "",
+ "distro to match against in the format: :",
+ )
+
+ flags.BoolVarP(&o.GenerateMissingCPEs,
+ "add-cpes-if-none", "",
+ "generate CPEs for packages with no CPE data",
+ )
+
+ flags.StringVarP(&o.OutputTemplateFile,
+ "template", "t",
+ "specify the path to a Go template file (requires 'template' output to be selected)")
+
+ flags.StringVarP(&o.FailOn,
+ "fail-on", "f",
+ fmt.Sprintf("set the return code to 1 if a vulnerability is found with a severity >= the given severity, options=%v", vulnerability.AllSeverities()),
+ )
+
+ flags.BoolVarP(&o.OnlyFixed,
+ "only-fixed", "",
+ "ignore matches for vulnerabilities that are not fixed",
+ )
+
+ flags.BoolVarP(&o.OnlyNotFixed,
+ "only-notfixed", "",
+ "ignore matches for vulnerabilities that are fixed",
+ )
+
+ flags.StringVarP(&o.IgnoreStates,
+ "ignore-states", "",
+ fmt.Sprintf("ignore matches for vulnerabilities with specified comma separated fix states, options=%v", vulnerability.AllFixStates()),
+ )
+
+ flags.BoolVarP(&o.ByCVE,
+ "by-cve", "",
+ "orient results by CVE instead of the original vulnerability ID when possible",
+ )
+
+ flags.BoolVarP(&o.ShowSuppressed,
+ "show-suppressed", "",
+ "show suppressed/ignored vulnerabilities in the output (only supported with table output format)",
+ )
+
+ flags.StringArrayVarP(&o.Exclusions,
+ "exclude", "",
+ "exclude paths from being scanned using a glob expression",
+ )
+
+ flags.StringVarP(&o.Platform,
+ "platform", "",
+ "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')",
+ )
+
+ flags.StringArrayVarP(&o.VexDocuments,
+ "vex", "",
+ "a list of VEX documents to consider when producing scanning results",
+ )
+}
+
+func (o *Grype) PostLoad() error {
+ if o.FailOn != "" {
+ failOnSeverity := *o.FailOnSeverity()
+ if failOnSeverity == vulnerability.UnknownSeverity {
+ return fmt.Errorf("bad --fail-on severity value '%s'", o.FailOn)
+ }
+ }
+ return nil
+}
+
+func (o *Grype) DescribeFields(descriptions clio.FieldDescriptionSet) {
+ descriptions.Add(&o.CheckForAppUpdate, `enable/disable checking for application updates on startup`)
+ descriptions.Add(&o.DefaultImagePullSource, `allows users to specify which image source should be used to generate the sbom
+valid values are: registry, docker, podman`)
+ descriptions.Add(&o.Name, `same as --name; set the name of the target being analyzed`)
+ descriptions.Add(&o.Exclusions, `a list of globs to exclude from scanning, for example:
+ - '/etc/**'
+ - './out/**/*.json'
+same as --exclude`)
+ descriptions.Add(&o.File, `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)`)
+ descriptions.Add(&o.Outputs, `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'`)
+ descriptions.Add(&o.Pretty, `pretty-print output`)
+ descriptions.Add(&o.FailOn, `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)`)
+ descriptions.Add(&o.Ignore, `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
+`)
+ descriptions.Add(&o.VexAdd, `VEX statuses to consider as ignored rules`)
+ descriptions.Add(&o.MatchUpstreamKernelHeaders, `match kernel-header packages with upstream kernel as kernel vulnerabilities`)
+}
+
+func (o Grype) FailOnSeverity() *vulnerability.Severity {
+ severity := vulnerability.ParseSeverity(o.FailOn)
+ return &severity
+}
diff --git a/cmd/grype/cli/options/match.go b/cmd/grype/cli/options/match.go
new file mode 100644
index 00000000000..b24df165b29
--- /dev/null
+++ b/cmd/grype/cli/options/match.go
@@ -0,0 +1,70 @@
+package options
+
+import "github.com/anchore/clio"
+
+// matchConfig contains all matching-related configuration options available to the user via the application config.
+type matchConfig struct {
+ Java matcherConfig `yaml:"java" json:"java" mapstructure:"java"` // settings for the java matcher
+ JVM matcherConfig `yaml:"jvm" json:"jvm" mapstructure:"jvm"` // settings for the jvm matcher
+ Dotnet matcherConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"` // settings for the dotnet matcher
+ Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"` // settings for the golang matcher
+ Javascript matcherConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"` // settings for the javascript matcher
+ Python matcherConfig `yaml:"python" json:"python" mapstructure:"python"` // settings for the python matcher
+ Ruby matcherConfig `yaml:"ruby" json:"ruby" mapstructure:"ruby"` // settings for the ruby matcher
+ Rust matcherConfig `yaml:"rust" json:"rust" mapstructure:"rust"` // settings for the rust matcher
+ Stock matcherConfig `yaml:"stock" json:"stock" mapstructure:"stock"` // settings for the default/stock matcher
+}
+
+var _ interface {
+ clio.FieldDescriber
+} = (*matchConfig)(nil)
+
+type matcherConfig struct {
+ UseCPEs bool `yaml:"using-cpes" json:"using-cpes" mapstructure:"using-cpes"` // if CPEs should be used during matching
+}
+
+type golangConfig struct {
+ matcherConfig `yaml:",inline" mapstructure:",squash"`
+ AlwaysUseCPEForStdlib bool `yaml:"always-use-cpe-for-stdlib" json:"always-use-cpe-for-stdlib" mapstructure:"always-use-cpe-for-stdlib"` // if CPEs should be used during matching
+ AllowMainModulePseudoVersionComparison bool `yaml:"allow-main-module-pseudo-version-comparison" json:"allow-main-module-pseudo-version-comparison" mapstructure:"allow-main-module-pseudo-version-comparison"` // if pseudo versions should be compared
+}
+
+func defaultGolangConfig() golangConfig {
+ return golangConfig{
+ matcherConfig: matcherConfig{
+ UseCPEs: false,
+ },
+ AlwaysUseCPEForStdlib: true,
+ AllowMainModulePseudoVersionComparison: false,
+ }
+}
+
+func defaultMatchConfig() matchConfig {
+ useCpe := matcherConfig{UseCPEs: true}
+ dontUseCpe := matcherConfig{UseCPEs: false}
+ return matchConfig{
+ Java: dontUseCpe,
+ JVM: useCpe,
+ Dotnet: dontUseCpe,
+ Golang: defaultGolangConfig(),
+ Javascript: dontUseCpe,
+ Python: dontUseCpe,
+ Ruby: dontUseCpe,
+ Rust: dontUseCpe,
+ Stock: useCpe,
+ }
+}
+
+func (cfg *matchConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
+ usingCpeDescription := `use CPE matching to find vulnerabilities`
+ descriptions.Add(&cfg.Java.UseCPEs, usingCpeDescription)
+ descriptions.Add(&cfg.Dotnet.UseCPEs, usingCpeDescription)
+ descriptions.Add(&cfg.Golang.UseCPEs, usingCpeDescription)
+ descriptions.Add(&cfg.Golang.AlwaysUseCPEForStdlib, usingCpeDescription+" for the Go standard library")
+ descriptions.Add(&cfg.Golang.AllowMainModulePseudoVersionComparison, `allow comparison between main module pseudo-versions (e.g. v0.0.0-20240413-2b432cf643...)`)
+ descriptions.Add(&cfg.Javascript.UseCPEs, usingCpeDescription)
+ descriptions.Add(&cfg.Python.UseCPEs, usingCpeDescription)
+ descriptions.Add(&cfg.Ruby.UseCPEs, usingCpeDescription)
+ descriptions.Add(&cfg.Rust.UseCPEs, usingCpeDescription)
+ descriptions.Add(&cfg.Stock.UseCPEs, usingCpeDescription)
+}
diff --git a/cmd/grype/cli/options/registry.go b/cmd/grype/cli/options/registry.go
new file mode 100644
index 00000000000..f32d48b4adb
--- /dev/null
+++ b/cmd/grype/cli/options/registry.go
@@ -0,0 +1,99 @@
+package options
+
+import (
+ "os"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/stereoscope/pkg/image"
+)
+
+type RegistryCredentials struct {
+ Authority string `yaml:"authority" json:"authority" mapstructure:"authority"`
+ // IMPORTANT: do not show the username, password, or token in any output (sensitive information)
+ Username secret `yaml:"username" json:"username" mapstructure:"username"`
+ Password secret `yaml:"password" json:"password" mapstructure:"password"`
+ Token secret `yaml:"token" json:"token" mapstructure:"token"`
+
+ TLSCert string `yaml:"tls-cert,omitempty" json:"tls-cert,omitempty" mapstructure:"tls-cert"`
+ TLSKey string `yaml:"tls-key,omitempty" json:"tls-key,omitempty" mapstructure:"tls-key"`
+}
+
+type registry struct {
+ InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
+ InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
+ Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
+ CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"`
+}
+
+var _ interface {
+ clio.PostLoader
+ clio.FieldDescriber
+} = (*registry)(nil)
+
+func (cfg *registry) PostLoad() error {
+ // there may be additional credentials provided by env var that should be appended to the set of credentials
+ authority, username, password, token, tlsCert, tlsKey :=
+ os.Getenv("GRYPE_REGISTRY_AUTH_AUTHORITY"),
+ os.Getenv("GRYPE_REGISTRY_AUTH_USERNAME"),
+ os.Getenv("GRYPE_REGISTRY_AUTH_PASSWORD"),
+ os.Getenv("GRYPE_REGISTRY_AUTH_TOKEN"),
+ os.Getenv("GRYPE_REGISTRY_AUTH_TLS_CERT"),
+ os.Getenv("GRYPE_REGISTRY_AUTH_TLS_KEY")
+
+ if hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey) {
+ // note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.
+ cfg.Auth = append([]RegistryCredentials{
+ {
+ Authority: authority,
+ Username: secret(username),
+ Password: secret(password),
+ Token: secret(token),
+ TLSCert: tlsCert,
+ TLSKey: tlsKey,
+ },
+ }, cfg.Auth...)
+ }
+ return nil
+}
+
+func (cfg *registry) DescribeFields(descriptions clio.FieldDescriptionSet) {
+ descriptions.Add(&cfg.InsecureSkipTLSVerify, "skip TLS verification when communicating with the registry")
+ descriptions.Add(&cfg.InsecureUseHTTP, "use http instead of https when connecting to the registry")
+ descriptions.Add(&cfg.CACert, "filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate")
+ descriptions.Add(&cfg.Auth, `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)
+`)
+}
+
+func hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey string) bool {
+ hasUserPass := username != "" && password != ""
+ hasToken := token != ""
+ hasTLSMaterial := tlsCert != "" && tlsKey != ""
+ return hasUserPass || hasToken || hasTLSMaterial
+}
+
+func (cfg *registry) ToOptions() *image.RegistryOptions {
+ var auth = make([]image.RegistryCredentials, len(cfg.Auth))
+ for i, a := range cfg.Auth {
+ auth[i] = image.RegistryCredentials{
+ Authority: a.Authority,
+ Username: a.Username.String(),
+ Password: a.Password.String(),
+ Token: a.Token.String(),
+ ClientCert: a.TLSCert,
+ ClientKey: a.TLSKey,
+ }
+ }
+
+ return &image.RegistryOptions{
+ InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
+ InsecureUseHTTP: cfg.InsecureUseHTTP,
+ Credentials: auth,
+ CAFileOrDir: cfg.CACert,
+ }
+}
diff --git a/internal/config/registry_test.go b/cmd/grype/cli/options/registry_test.go
similarity index 62%
rename from internal/config/registry_test.go
rename to cmd/grype/cli/options/registry_test.go
index d14769bcf18..4979fcbc281 100644
--- a/internal/config/registry_test.go
+++ b/cmd/grype/cli/options/registry_test.go
@@ -1,4 +1,4 @@
-package config
+package options
import (
"fmt"
@@ -11,47 +11,60 @@ import (
func TestHasNonEmptyCredentials(t *testing.T) {
tests := []struct {
- username, password, token string
- expected bool
+ username, password, token, cert, key string
+ expected bool
}{
+
{
- "", "", "",
+ "", "", "", "", "",
false,
},
{
- "user", "", "",
+ "user", "", "", "", "",
false,
},
{
- "", "pass", "",
+ "", "pass", "", "", "",
false,
},
{
- "", "pass", "tok",
+ "", "pass", "tok", "", "",
true,
},
{
- "user", "", "tok",
+ "user", "", "tok", "", "",
true,
},
{
- "", "", "tok",
+ "", "", "tok", "", "",
true,
},
{
- "user", "pass", "tok",
+ "user", "pass", "tok", "", "",
true,
},
{
- "user", "pass", "",
+ "user", "pass", "", "", "",
true,
},
+ {
+ "", "", "", "cert", "key",
+ true,
+ },
+ {
+ "", "", "", "cert", "",
+ false,
+ },
+ {
+ "", "", "", "", "key",
+ false,
+ },
}
for _, test := range tests {
t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
- assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token))
+ assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token, test.cert, test.key))
})
}
}
@@ -101,6 +114,29 @@ func Test_registry_ToOptions(t *testing.T) {
Credentials: []image.RegistryCredentials{},
},
},
+ {
+ name: "provide all tls configuration",
+ input: registry{
+ CACert: "ca.crt",
+ InsecureSkipTLSVerify: true,
+ Auth: []RegistryCredentials{
+ {
+ TLSCert: "client.crt",
+ TLSKey: "client.key",
+ },
+ },
+ },
+ expected: image.RegistryOptions{
+ CAFileOrDir: "ca.crt",
+ InsecureSkipTLSVerify: true,
+ Credentials: []image.RegistryCredentials{
+ {
+ ClientCert: "client.crt",
+ ClientKey: "client.key",
+ },
+ },
+ },
+ },
}
for _, test := range tests {
diff --git a/cmd/grype/cli/options/search.go b/cmd/grype/cli/options/search.go
new file mode 100644
index 00000000000..1a8523d1365
--- /dev/null
+++ b/cmd/grype/cli/options/search.go
@@ -0,0 +1,49 @@
+package options
+
+import (
+ "fmt"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/syft/syft/cataloging"
+ "github.com/anchore/syft/syft/source"
+)
+
+type search struct {
+ Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
+ IncludeUnindexedArchives bool `yaml:"unindexed-archives" json:"unindexed-archives" mapstructure:"unindexed-archives"`
+ IncludeIndexedArchives bool `yaml:"indexed-archives" json:"indexed-archives" mapstructure:"indexed-archives"`
+}
+
+var _ interface {
+ clio.PostLoader
+ clio.FieldDescriber
+} = (*search)(nil)
+
+func defaultSearch(scope source.Scope) search {
+ c := cataloging.DefaultArchiveSearchConfig()
+ return search{
+ Scope: scope.String(),
+ IncludeUnindexedArchives: c.IncludeUnindexedArchives,
+ IncludeIndexedArchives: c.IncludeIndexedArchives,
+ }
+}
+
+func (cfg *search) PostLoad() error {
+ scopeOption := cfg.GetScope()
+ if scopeOption == source.UnknownScope {
+ return fmt.Errorf("bad scope value %q", cfg.Scope)
+ }
+ return nil
+}
+
+func (cfg *search) DescribeFields(descriptions clio.FieldDescriptionSet) {
+ descriptions.Add(&cfg.IncludeIndexedArchives, `search within archives that do contain a file index to search against (zip)
+note: for now this only applies to the java package cataloger`)
+ descriptions.Add(&cfg.IncludeUnindexedArchives, `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`)
+}
+
+func (cfg search) GetScope() source.Scope {
+ return source.ParseScope(cfg.Scope)
+}
diff --git a/cmd/grype/cli/options/secret.go b/cmd/grype/cli/options/secret.go
new file mode 100644
index 00000000000..dcdf2b905df
--- /dev/null
+++ b/cmd/grype/cli/options/secret.go
@@ -0,0 +1,25 @@
+package options
+
+import (
+ "fmt"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/internal/redact"
+)
+
+type secret string
+
+var _ interface {
+ fmt.Stringer
+ clio.PostLoader
+} = (*secret)(nil)
+
+// PostLoad needs to use a pointer receiver, even if it's not modifying the value
+func (r *secret) PostLoad() error {
+ redact.Add(string(*r))
+ return nil
+}
+
+func (r secret) String() string {
+ return string(r)
+}
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/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap b/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap
index 6a0244c4192..2d424d73be9 100755
--- a/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap
+++ b/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap
@@ -1,18 +1,18 @@
[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/task_line - 1]
- ⠋ Scanning for vulnerabilities [20 vulnerabilities]
+ ⠋ Scanning for vulnerabilities [36 vulnerability matches]
---
[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/tree - 1]
- ├── 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown)
- └── 30 fixed
+ ├── by severity: 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown)
+ └── by status: 30 fixed, 10 not-fixed, 4 ignored (2 dropped)
---
[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/task_line - 1]
- ✔ Scanned for vulnerabilities [25 vulnerabilities]
+ ✔ Scanned for vulnerabilities [40 vulnerability matches]
---
[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/tree - 1]
- ├── 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown)
- └── 35 fixed
+ ├── by severity: 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown)
+ └── by status: 35 fixed, 10 not-fixed, 5 ignored (3 dropped)
---
diff --git a/cmd/grype/cli/ui/handle_database_diff_started.go b/cmd/grype/cli/ui/handle_database_diff_started.go
index c8dbecd4a3e..b776e2b6581 100644
--- a/cmd/grype/cli/ui/handle_database_diff_started.go
+++ b/cmd/grype/cli/ui/handle_database_diff_started.go
@@ -36,11 +36,11 @@ func (p dbDiffProgressStager) Size() int64 {
return p.monitor.StageProgress.Size()
}
-func (m *Handler) handleDatabaseDiffStarted(e partybus.Event) []tea.Model {
+func (m *Handler) handleDatabaseDiffStarted(e partybus.Event) ([]tea.Model, tea.Cmd) {
mon, err := parsers.ParseDatabaseDiffingStarted(e)
if err != nil {
log.WithFields("error", err).Warn("unable to parse event")
- return nil
+ return nil, nil
}
tsk := m.newTaskProgress(
@@ -54,5 +54,5 @@ func (m *Handler) handleDatabaseDiffStarted(e partybus.Event) []tea.Model {
tsk.HideStageOnSuccess = false
- return []tea.Model{tsk}
+ return []tea.Model{tsk}, nil
}
diff --git a/cmd/grype/cli/ui/handle_database_diff_started_test.go b/cmd/grype/cli/ui/handle_database_diff_started_test.go
index b890f791ead..3cfabc2f31d 100644
--- a/cmd/grype/cli/ui/handle_database_diff_started_test.go
+++ b/cmd/grype/cli/ui/handle_database_diff_started_test.go
@@ -77,7 +77,7 @@ func TestHandler_handleDatabaseDiffStarted(t *testing.T) {
Height: 80,
}
- models := handler.Handle(e)
+ models, _ := handler.Handle(e)
require.Len(t, models, 1)
model := models[0]
diff --git a/cmd/grype/cli/ui/handle_update_vulnerability_database.go b/cmd/grype/cli/ui/handle_update_vulnerability_database.go
index 938ae9e1c07..6547b97bd53 100644
--- a/cmd/grype/cli/ui/handle_update_vulnerability_database.go
+++ b/cmd/grype/cli/ui/handle_update_vulnerability_database.go
@@ -27,16 +27,19 @@ func (s dbDownloadProgressStager) Stage() string {
return "validating"
}
// show intermediate progress of the download
- return fmt.Sprintf("%s / %s", humanize.Bytes(uint64(s.prog.Current())), humanize.Bytes(uint64(s.prog.Size())))
+ return fmt.Sprintf("%s / %s",
+ humanize.Bytes(safeConvertInt64ToUint64(s.prog.Current())),
+ humanize.Bytes(safeConvertInt64ToUint64(s.prog.Size())),
+ )
}
return stage
}
-func (m *Handler) handleUpdateVulnerabilityDatabase(e partybus.Event) []tea.Model {
+func (m *Handler) handleUpdateVulnerabilityDatabase(e partybus.Event) ([]tea.Model, tea.Cmd) {
prog, err := parsers.ParseUpdateVulnerabilityDatabase(e)
if err != nil {
log.WithFields("error", err).Warn("unable to parse event")
- return nil
+ return nil, nil
}
tsk := m.newTaskProgress(
@@ -49,5 +52,12 @@ func (m *Handler) handleUpdateVulnerabilityDatabase(e partybus.Event) []tea.Mode
tsk.HideStageOnSuccess = false
- return []tea.Model{tsk}
+ return []tea.Model{tsk}, nil
+}
+
+func safeConvertInt64ToUint64(i int64) uint64 {
+ if i < 0 {
+ return 0
+ }
+ return uint64(i)
}
diff --git a/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go b/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go
index c3b64e1585f..21be4cff8f0 100644
--- a/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go
+++ b/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go
@@ -78,7 +78,7 @@ func TestHandler_handleUpdateVulnerabilityDatabase(t *testing.T) {
Height: 80,
}
- models := handler.Handle(e)
+ models, _ := handler.Handle(e)
require.Len(t, models, 1)
model := models[0]
diff --git a/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go b/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go
index 54ef042bcb8..569abd26b4b 100644
--- a/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go
+++ b/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go
@@ -32,6 +32,9 @@ type vulnerabilityProgressTree struct {
countBySeverity map[vulnerability.Severity]int64
unknownCount int64
fixedCount int64
+ ignoredCount int64
+ droppedCount int64
+ totalCount int64
severities []vulnerability.Severity
id uint32
@@ -65,26 +68,26 @@ type vulnerabilityScanningAdapter struct {
}
func (p vulnerabilityScanningAdapter) Current() int64 {
- return p.mon.VulnerabilitiesDiscovered.Current()
+ return p.mon.PackagesProcessed.Current()
}
func (p vulnerabilityScanningAdapter) Error() error {
- return p.mon.VulnerabilitiesDiscovered.Error()
+ return p.mon.MatchesDiscovered.Error()
}
func (p vulnerabilityScanningAdapter) Size() int64 {
- return -1
+ return p.mon.PackagesProcessed.Size()
}
func (p vulnerabilityScanningAdapter) Stage() string {
- return fmt.Sprintf("%d vulnerabilities", p.mon.VulnerabilitiesDiscovered.Current())
+ return fmt.Sprintf("%d vulnerability matches", p.mon.MatchesDiscovered.Current()-p.mon.Ignored.Current())
}
-func (m *Handler) handleVulnerabilityScanningStarted(e partybus.Event) []tea.Model {
+func (m *Handler) handleVulnerabilityScanningStarted(e partybus.Event) ([]tea.Model, tea.Cmd) {
mon, err := parsers.ParseVulnerabilityScanningStarted(e)
if err != nil {
log.WithFields("error", err).Warn("unable to parse event")
- return nil
+ return nil, nil
}
tsk := m.newTaskProgress(
@@ -103,7 +106,7 @@ func (m *Handler) handleVulnerabilityScanningStarted(e partybus.Event) []tea.Mod
return []tea.Model{
tsk,
newVulnerabilityProgressTree(mon, textStyle),
- }
+ }, nil
}
func (l vulnerabilityProgressTree) Init() tea.Cmd {
@@ -131,7 +134,10 @@ func (l vulnerabilityProgressTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case vulnerabilityProgressTreeTickMsg:
// update the model
+ l.totalCount = l.mon.MatchesDiscovered.Current()
l.fixedCount = l.mon.Fixed.Current()
+ l.ignoredCount = l.mon.Ignored.Current()
+ l.droppedCount = l.mon.Dropped.Current()
l.unknownCount = l.mon.BySeverity[vulnerability.UnknownSeverity].Current()
for _, sev := range l.severities {
l.countBySeverity[sev] = l.mon.BySeverity[sev].Current()
@@ -164,12 +170,21 @@ func (l vulnerabilityProgressTree) View() string {
status := sb.String()
sb.Reset()
- sevStr := l.textStyle.Render(fmt.Sprintf(" %s %s", branch, status))
- fixedStr := l.textStyle.Render(fmt.Sprintf(" %s %d fixed", end, l.fixedCount))
+ sevStr := l.textStyle.Render(fmt.Sprintf(" %s by severity: %s", branch, status))
sb.WriteString(sevStr)
- sb.WriteString("\n")
- sb.WriteString(fixedStr)
+
+ dropped := ""
+ if l.droppedCount > 0 {
+ dropped = fmt.Sprintf("(%d dropped)", l.droppedCount)
+ }
+
+ fixedStr := l.textStyle.Render(
+ fmt.Sprintf(" %s by status: %d fixed, %d not-fixed, %d ignored %s",
+ end, l.fixedCount, l.totalCount-l.fixedCount, l.ignoredCount, dropped,
+ ),
+ )
+ sb.WriteString("\n" + fixedStr)
return sb.String()
}
diff --git a/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go b/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go
index ab23fd7d4fd..d4e8f2393fd 100644
--- a/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go
+++ b/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go
@@ -51,7 +51,7 @@ func TestHandler_handleVulnerabilityScanningStarted(t *testing.T) {
Height: 80,
}
- models := handler.Handle(e)
+ models, _ := handler.Handle(e)
require.Len(t, models, 2)
t.Run("task line", func(t *testing.T) {
@@ -96,10 +96,10 @@ func getVulnerabilityMonitor(completed bool) monitor.Matching {
vulns := &progress.Manual{}
vulns.SetTotal(-1)
if completed {
- vulns.Set(25)
+ vulns.Set(45)
vulns.SetCompleted()
} else {
- vulns.Set(20)
+ vulns.Set(40)
}
fixed := &progress.Manual{}
@@ -111,6 +111,24 @@ func getVulnerabilityMonitor(completed bool) monitor.Matching {
fixed.Set(30)
}
+ ignored := &progress.Manual{}
+ ignored.SetTotal(-1)
+ if completed {
+ ignored.Set(5)
+ ignored.SetCompleted()
+ } else {
+ ignored.Set(4)
+ }
+
+ dropped := &progress.Manual{}
+ dropped.SetTotal(-1)
+ if completed {
+ dropped.Set(3)
+ dropped.SetCompleted()
+ } else {
+ dropped.Set(2)
+ }
+
bySeverityWriter := map[vulnerability.Severity]*progress.Manual{
vulnerability.CriticalSeverity: {},
vulnerability.HighSeverity: {},
@@ -137,9 +155,11 @@ func getVulnerabilityMonitor(completed bool) monitor.Matching {
}
return monitor.Matching{
- PackagesProcessed: pkgs,
- VulnerabilitiesDiscovered: vulns,
- Fixed: fixed,
- BySeverity: bySeverity,
+ PackagesProcessed: pkgs,
+ MatchesDiscovered: vulns,
+ Fixed: fixed,
+ Ignored: ignored,
+ Dropped: dropped,
+ BySeverity: bySeverity,
}
}
diff --git a/cmd/grype/internal/constants.go b/cmd/grype/internal/constants.go
new file mode 100644
index 00000000000..eedbdb0eeb2
--- /dev/null
+++ b/cmd/grype/internal/constants.go
@@ -0,0 +1,5 @@
+package internal
+
+const (
+ NotProvided = "[not provided]"
+)
diff --git a/cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap b/cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap
index 05ee3a5a291..32b9950e43e 100755
--- a/cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap
+++ b/cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap
@@ -27,12 +27,7 @@ report 1!!>
-
-
-
-
-
+A newer version of grype is available for download: v0.33.0 (installed version is [not provided])
---
@@ -43,4 +38,4 @@ report 1!!>
[Test_postUIEventWriter_write/quiet_only_shows_report/stderr - 1]
----
\ No newline at end of file
+---
diff --git a/cmd/grype/internal/ui/no_ui.go b/cmd/grype/internal/ui/no_ui.go
index e78489fcf78..a05b2f49de7 100644
--- a/cmd/grype/internal/ui/no_ui.go
+++ b/cmd/grype/internal/ui/no_ui.go
@@ -33,8 +33,6 @@ func (n *NoUI) Handle(e partybus.Event) error {
case event.CLIReport, event.CLINotification:
// keep these for when the UI is terminated to show to the screen (or perform other events)
n.finalizeEvents = append(n.finalizeEvents, e)
- case event.CLIExit:
- return n.subscription.Unsubscribe()
}
return nil
}
diff --git a/cmd/grype/internal/ui/post_ui_event_writer.go b/cmd/grype/internal/ui/post_ui_event_writer.go
index c008195e1fc..f6949daa846 100644
--- a/cmd/grype/internal/ui/post_ui_event_writer.go
+++ b/cmd/grype/internal/ui/post_ui_event_writer.go
@@ -118,12 +118,18 @@ func writeAppUpdate(writer io.Writer, events ...partybus.Event) error {
style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true)
for _, e := range events {
- notice, err := parsers.ParseCLIAppUpdateAvailable(e)
+ version, err := parsers.ParseCLIAppUpdateAvailable(e)
if err != nil {
log.WithFields("error", err).Warn("failed to parse app update notification")
continue
}
+ if version.New == "" {
+ continue
+ }
+
+ notice := fmt.Sprintf("A newer version of grype is available for download: %s (installed version is %s)", version.New, version.Current)
+
if _, err := fmt.Fprintln(writer, style.Render(notice)); err != nil {
// don't let this be fatal
log.WithFields("error", err).Warn("failed to write app update notification")
diff --git a/cmd/grype/internal/ui/post_ui_event_writer_test.go b/cmd/grype/internal/ui/post_ui_event_writer_test.go
index 1d8509de6cc..329ce2b71f8 100644
--- a/cmd/grype/internal/ui/post_ui_event_writer_test.go
+++ b/cmd/grype/internal/ui/post_ui_event_writer_test.go
@@ -9,6 +9,7 @@ import (
"github.com/wagoodman/go-partybus"
"github.com/anchore/grype/grype/event"
+ "github.com/anchore/grype/grype/event/parsers"
)
func Test_postUIEventWriter_write(t *testing.T) {
@@ -34,8 +35,11 @@ func Test_postUIEventWriter_write(t *testing.T) {
Value: "",
},
{
- Type: event.CLIAppUpdateAvailable,
- Value: "\n\n\n\n",
+ Type: event.CLIAppUpdateAvailable,
+ Value: parsers.UpdateCheck{
+ New: "v0.33.0",
+ Current: "[not provided]",
+ },
},
{
Type: event.CLINotification,
@@ -61,8 +65,11 @@ func Test_postUIEventWriter_write(t *testing.T) {
Value: "",
},
{
- Type: event.CLIAppUpdateAvailable,
- Value: "",
+ Type: event.CLIAppUpdateAvailable,
+ Value: parsers.UpdateCheck{
+ New: "",
+ Current: "",
+ },
},
{
Type: event.CLIReport,
diff --git a/cmd/grype/internal/ui/select.go b/cmd/grype/internal/ui/select.go
deleted file mode 100644
index 5628907ea36..00000000000
--- a/cmd/grype/internal/ui/select.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package ui
-
-import (
- "os"
- "runtime"
-
- "golang.org/x/term"
-
- "github.com/anchore/clio"
- grypeHandler "github.com/anchore/grype/cmd/grype/cli/ui"
- syftHandler "github.com/anchore/syft/cmd/syft/cli/ui"
-)
-
-// Select is responsible for determining the specific UI function given select user option, the current platform
-// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs
-// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
-// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
-// the final SBOM report.
-func Select(verbose, quiet bool) (uis []clio.UI) {
- isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
- isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
- notATerminal := !isStderrATty && !isStdoutATty
-
- switch {
- case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
- uis = append(uis, None(quiet))
- default:
- // TODO: it may make sense in the future to pass handler options into select
-
- uis = append(uis,
- New(
- verbose, quiet,
- grypeHandler.New(grypeHandler.DefaultHandlerConfig()),
- syftHandler.New(syftHandler.DefaultHandlerConfig()),
- ),
- )
- }
-
- return uis
-}
diff --git a/cmd/grype/internal/ui/ui.go b/cmd/grype/internal/ui/ui.go
index 5814e8033a4..78349fc9349 100644
--- a/cmd/grype/internal/ui/ui.go
+++ b/cmd/grype/internal/ui/ui.go
@@ -1,8 +1,10 @@
package ui
import (
+ "fmt"
"os"
"sync"
+ "time"
tea "github.com/charmbracelet/bubbletea"
"github.com/wagoodman/go-partybus"
@@ -33,9 +35,9 @@ type UI struct {
frame tea.Model
}
-func New(_, quiet bool, hs ...bubbly.EventHandler) *UI {
+func New(quiet bool, handlers ...bubbly.EventHandler) *UI {
return &UI{
- handler: bubbly.NewHandlerCollection(hs...),
+ handler: bubbly.NewHandlerCollection(handlers...),
frame: frame.New(),
running: &sync.WaitGroup{},
quiet: quiet,
@@ -56,37 +58,51 @@ func (m *UI) Setup(subscription partybus.Unsubscribable) error {
defer m.running.Done()
if _, err := m.program.Run(); err != nil {
log.Errorf("unable to start UI: %+v", err)
- m.exit()
+ bus.ExitWithInterrupt()
}
}()
return nil
}
-func (m *UI) exit() {
- // stop the event loop
- bus.Exit()
-}
-
func (m *UI) Handle(e partybus.Event) error {
if m.program != nil {
m.program.Send(e)
- if e.Type == event.CLIExit {
- return m.subscription.Unsubscribe()
- }
}
return nil
}
func (m *UI) Teardown(force bool) error {
+ defer func() {
+ // allow for traditional logging to resume now that the UI is shutting down
+ if logWrapper, ok := log.Get().(logger.Controller); ok {
+ logWrapper.SetOutput(os.Stderr)
+ }
+ }()
+
if !force {
m.handler.Wait()
m.program.Quit()
+ // typically in all cases we would want to wait for the UI to finish. However there are still error cases
+ // that are not accounted for, resulting in hangs. For now, we'll just wait for the UI to finish in the
+ // happy path only. There will always be an indication of the problem to the user via reporting the error
+ // string from the worker (outside of the UI after teardown).
+ m.running.Wait()
} else {
- m.program.Kill()
- }
+ _ = runWithTimeout(250*time.Millisecond, func() error {
+ m.handler.Wait()
+ return nil
+ })
+
+ // it may be tempting to use Kill() however it has been found that this can cause the terminal to be left in
+ // a bad state (where Ctrl+C and other control characters no longer works for future processes in that terminal).
+ m.program.Quit()
- m.running.Wait()
+ _ = runWithTimeout(250*time.Millisecond, func() error {
+ m.running.Wait()
+ return nil
+ })
+ }
// TODO: allow for writing out the full log output to the screen (only a partial log is shown currently)
// this needs coordination to know what the last frame event is to change the state accordingly (which isn't possible now)
@@ -104,7 +120,6 @@ func (m UI) RespondsTo() []partybus.EventType {
return append([]partybus.EventType{
event.CLIReport,
event.CLINotification,
- event.CLIExit,
event.CLIAppUpdateAvailable,
}, m.handler.RespondsTo()...)
}
@@ -123,8 +138,10 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
+ // today we treat esc and ctrl+c the same, but in the future when the worker has a graceful way to
+ // cancel in-flight work via a context, we can wire up esc to this path with bus.Exit()
case "esc", "ctrl+c":
- m.exit()
+ bus.ExitWithInterrupt()
return m, tea.Quit
}
@@ -132,16 +149,20 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.WithFields("component", "ui").Tracef("event: %q", msg.Type)
switch msg.Type {
- case event.CLIReport, event.CLINotification, event.CLIExit, event.CLIAppUpdateAvailable:
+ case event.CLIReport, event.CLINotification, event.CLIAppUpdateAvailable:
// keep these for when the UI is terminated to show to the screen (or perform other events)
m.finalizeEvents = append(m.finalizeEvents, msg)
// why not return tea.Quit here for exit events? because there may be UI components that still need the update-render loop.
- // for this reason we'll let the syft event loop call Teardown() which will explicitly wait for these components
+ // for this reason we'll let the event loop call Teardown() which will explicitly wait for these components
return m, nil
}
- for _, newModel := range m.handler.Handle(msg) {
+ models, cmd := m.handler.Handle(msg)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ for _, newModel := range models {
if newModel == nil {
continue
}
@@ -161,3 +182,17 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m UI) View() string {
return m.frame.View()
}
+
+func runWithTimeout(timeout time.Duration, fn func() error) (err error) {
+ c := make(chan struct{}, 1)
+ go func() {
+ err = fn()
+ c <- struct{}{}
+ }()
+ select {
+ case <-c:
+ case <-time.After(timeout):
+ return fmt.Errorf("timed out after %v", timeout)
+ }
+ return err
+}
diff --git a/cmd/grype/main.go b/cmd/grype/main.go
index e4cb2d5f579..f3f11363fdd 100644
--- a/cmd/grype/main.go
+++ b/cmd/grype/main.go
@@ -1,9 +1,34 @@
package main
import (
- "github.com/anchore/grype/cmd/grype/cli/legacy"
+ _ "github.com/glebarez/sqlite"
+
+ "github.com/anchore/clio"
+ "github.com/anchore/grype/cmd/grype/cli"
+ "github.com/anchore/grype/cmd/grype/internal"
+)
+
+// applicationName is the non-capitalized name of the application (do not change this)
+const applicationName = "grype"
+
+// all variables here are provided as build-time arguments, with clear default values
+var (
+ version = internal.NotProvided
+ buildDate = internal.NotProvided
+ gitCommit = internal.NotProvided
+ gitDescription = internal.NotProvided
)
func main() {
- legacy.Execute()
+ app := cli.Application(
+ clio.Identification{
+ Name: applicationName,
+ Version: version,
+ BuildDate: buildDate,
+ GitCommit: gitCommit,
+ GitDescription: gitDescription,
+ },
+ )
+
+ app.Run()
}
diff --git a/go.mod b/go.mod
index 946fe252c85..eb8461a985e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,227 +1,308 @@
module github.com/anchore/grype
-go 1.19
+go 1.24.1
require (
- github.com/CycloneDX/cyclonedx-go v0.7.1
- github.com/Masterminds/sprig/v3 v3.2.3
+ github.com/CycloneDX/cyclonedx-go v0.9.2
+ github.com/Masterminds/sprig/v3 v3.3.0
+ github.com/OneOfOne/xxhash v1.2.8
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
- github.com/adrg/xdg v0.4.0
+ 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-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-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.20230104203445-02e0a6721501
- github.com/anchore/stereoscope v0.0.0-20230627195312-cd49355d934e
+ github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115
+ github.com/anchore/stereoscope v0.1.4
+ github.com/anchore/syft v1.26.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/docker/docker v24.0.4+incompatible
+ 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.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.2
- github.com/go-test/deep v1.1.0
- github.com/google/go-cmp v0.5.9
- github.com/google/uuid v1.3.0
- github.com/gookit/color v1.5.3
+ 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.5
+ github.com/google/uuid v1.6.0
+ github.com/gookit/color v1.5.4
+ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/hashicorp/go-cleanhttp v0.5.2
- github.com/hashicorp/go-getter v1.7.1
+ github.com/hashicorp/go-getter v1.7.8
github.com/hashicorp/go-multierror v1.1.1
- github.com/hashicorp/go-version v1.6.0
+ github.com/hashicorp/go-version v1.7.0
+ github.com/iancoleman/strcase v0.3.0
+ github.com/invopop/jsonschema v0.13.0
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d
- github.com/mholt/archiver/v3 v3.5.1
- github.com/mitchellh/go-homedir v1.1.0
- github.com/mitchellh/hashstructure/v2 v2.0.2
+ github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08
+ github.com/muesli/termenv v0.16.0
github.com/olekukonko/tablewriter v0.0.5
- github.com/owenrumney/go-sarif v1.1.1
- github.com/pkg/profile v1.7.0
+ github.com/openvex/go-vex v0.2.5
+ github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554
+ github.com/pandatix/go-cvss v0.6.2
// 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.1
- github.com/sirupsen/logrus v1.9.3
- github.com/spf13/afero v1.9.5
- github.com/spf13/cobra v1.7.0
- github.com/spf13/pflag v1.0.5
- github.com/spf13/viper v1.16.0
- github.com/stretchr/testify v1.8.4
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
+ 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
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
- github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5
- github.com/x-cray/logrus-prefixed-formatter v0.5.2
- golang.org/x/term v0.10.0
- gopkg.in/yaml.v2 v2.4.0
- gorm.io/gorm v1.23.10
-)
-
-require (
- github.com/anchore/bubbly v0.0.0-20230712165553-812110ab0a10
- github.com/anchore/clio v0.0.0-20230630162005-9535e9dc2817
- github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe
- github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963
- github.com/anchore/syft v0.85.0
- github.com/charmbracelet/bubbletea v0.24.2
- github.com/charmbracelet/lipgloss v0.7.1
- github.com/gkampitakis/go-snaps v0.4.8
- github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
- github.com/mitchellh/mapstructure v1.5.0
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-20250305212735-054e65f0b394
+ golang.org/x/time v0.11.0
+ golang.org/x/tools v0.33.0
+ gopkg.in/yaml.v3 v3.0.1
+ gorm.io/gorm v1.30.0
)
require (
- cloud.google.com/go v0.110.0 // indirect
- cloud.google.com/go/compute v1.19.1 // indirect
- cloud.google.com/go/compute/metadata v0.2.3 // indirect
- cloud.google.com/go/iam v0.13.0 // indirect
- cloud.google.com/go/storage v1.28.1 // indirect
- github.com/DataDog/zstd v1.4.5 // indirect
+ cel.dev/expr v0.16.1 // indirect
+ cloud.google.com/go v0.116.0 // indirect
+ cloud.google.com/go/auth v0.13.0 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
+ cloud.google.com/go/compute/metadata v0.6.0 // indirect
+ cloud.google.com/go/iam v1.2.2 // indirect
+ cloud.google.com/go/monitoring v1.21.2 // indirect
+ cloud.google.com/go/storage v1.49.0 // indirect
+ 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.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.2.0 // indirect
- github.com/Microsoft/go-winio v0.6.1 // indirect
- github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // 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.2.0 // indirect
+ github.com/STARRY-S/zip v0.2.1 // indirect
github.com/acobaugh/osrelease v0.1.0 // indirect
- github.com/acomagu/bufpipe v1.0.4 // indirect
- github.com/anchore/fangs v0.0.0-20230628163043-a51c5a39b097 // indirect
+ github.com/agext/levenshtein v1.2.1 // 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.0.4 // indirect
- github.com/aws/aws-sdk-go v1.44.180 // 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
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/becheran/wildmatch-go v1.0.0 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
- github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
- github.com/charmbracelet/bubbles v0.16.1 // 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.21.0 // indirect
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
- github.com/cloudflare/circl v1.3.3 // indirect
- github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
- github.com/containerd/containerd v1.7.0 // indirect
- github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
- github.com/davecgh/go-spew v1.1.1 // 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.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.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.7 // indirect
+ github.com/containerd/typeurl/v2 v2.1.1 // 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/docker/cli v23.0.5+incompatible // indirect
- github.com/docker/distribution v2.8.2+incompatible // indirect
- github.com/docker/docker-credential-helpers v0.7.0 // indirect
- github.com/docker/go-connections v0.4.0 // indirect
+ github.com/distribution/reference v0.6.0 // 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.9.3 // 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/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/felixge/fgprof v0.9.3 // indirect
- github.com/fsnotify/fsnotify v1.6.0 // indirect
- github.com/github/go-spdx/v2 v2.1.2 // indirect
- github.com/gkampitakis/ciinfo v0.2.4 // indirect
+ github.com/envoyproxy/go-control-plane v0.13.1 // indirect
+ github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
+ 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.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.22.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
- github.com/go-git/go-billy/v5 v5.4.1 // indirect
- github.com/go-git/go-git/v5 v5.7.0 // indirect
+ github.com/go-git/go-billy/v5 v5.6.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/protobuf v1.5.3 // indirect
+ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v0.0.4 // indirect
- github.com/google/go-containerregistry v0.15.2 // indirect
github.com/google/licensecheck v0.3.1 // indirect
- github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
- github.com/google/s2a-go v0.1.3 // indirect
- github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
- github.com/googleapis/gax-go/v2 v2.8.0 // 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/huandu/xstrings v1.3.3 // indirect
- github.com/imdario/mergo v0.3.15 // 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
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
- github.com/jinzhu/copier v0.3.5 // indirect
+ github.com/jinzhu/copier v0.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
- github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // 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.16.5 // indirect
- github.com/klauspost/pgzip v1.2.5 // indirect
- github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
- github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // 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.7 // indirect
- github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.18 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/maruel/natural v1.1.1 // 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.14 // 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 v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
- github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // 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.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
+ github.com/moby/term v0.5.0 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
- github.com/muesli/reflow v0.3.0 // indirect
- github.com/muesli/termenv v0.15.1 // indirect
- github.com/nwaples/rardecode v1.1.0 // indirect
- github.com/onsi/ginkgo v1.16.5 // indirect
- github.com/onsi/gomega v1.19.0 // 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-rc3 // 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.0.8 // indirect
- github.com/pierrec/lz4/v4 v4.1.15 // indirect
- github.com/pjbgf/sha1cd v0.3.0 // 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/pmezard/go-difflib v1.0.0 // indirect
+ github.com/pkg/profile v1.7.0 // indirect
+ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
+ 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.2.0 // indirect
- github.com/rogpeppe/go-internal v1.9.0 // indirect
- github.com/sassoftware/go-rpmutils v0.2.0 // indirect
- github.com/shopspring/decimal v1.2.0 // indirect
- github.com/skeema/knownhosts v1.1.1 // indirect
- github.com/spdx/tools-golang v0.5.2 // indirect
- github.com/spf13/cast v1.5.1 // indirect
- github.com/spf13/jwalterweatherman v1.1.0 // indirect
- github.com/stretchr/objx v0.5.0 // indirect
- github.com/subosito/gotenv v1.4.2 // indirect
- github.com/sylabs/sif/v2 v2.8.1 // indirect
- github.com/sylabs/squashfs v0.6.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // 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/shopspring/decimal v1.4.0 // indirect
+ github.com/sirupsen/logrus v1.9.3 // 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.1 // indirect
+ github.com/spf13/pflag v1.0.6 // 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.21.1 // indirect
+ github.com/sylabs/squashfs v1.0.6 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
- github.com/tidwall/gjson v1.14.4 // indirect
+ github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
- github.com/ulikunitz/xz v0.5.10 // indirect
- github.com/vbatts/go-mtree v0.5.3 // indirect
- github.com/vbatts/tar-split v0.11.3 // indirect
- github.com/vifraa/gopom v0.2.1 // indirect
+ github.com/vbatts/go-mtree v0.5.4 // indirect
+ github.com/vbatts/tar-split v0.12.1 // indirect
+ github.com/vifraa/gopom v1.0.0 // indirect
+ github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
- github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
- github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
- github.com/zclconf/go-cty v1.10.0 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/zclconf/go-cty v1.14.0 // indirect
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 // indirect
go.opencensus.io v0.24.0 // indirect
- go.uber.org/goleak v1.2.0 // indirect
- golang.org/x/crypto v0.11.0 // indirect
- golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b // indirect
- golang.org/x/mod v0.12.0 // indirect
- golang.org/x/net v0.12.0 // indirect
- golang.org/x/oauth2 v0.7.0 // indirect
- golang.org/x/sync v0.1.0 // indirect
- golang.org/x/sys v0.10.0 // indirect
- golang.org/x/text v0.11.0 // indirect
- golang.org/x/time v0.2.0 // indirect
- golang.org/x/tools v0.8.0 // indirect
- golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
- google.golang.org/api v0.122.0 // indirect
- google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
- google.golang.org/grpc v1.55.0 // indirect
- google.golang.org/protobuf v1.30.0 // indirect
- gopkg.in/ini.v1 v1.67.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+ go.opentelemetry.io/otel v1.35.0 // indirect
+ go.opentelemetry.io/otel/metric v1.35.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.35.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
+ go.opentelemetry.io/otel/trace v1.35.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.40.0 // indirect
+ golang.org/x/oauth2 v0.30.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.4 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
- lukechampine.com/uint128 v1.3.0 // indirect
- modernc.org/cc/v3 v3.40.0 // indirect
- modernc.org/ccgo/v3 v3.16.13 // indirect
- modernc.org/libc v1.22.5 // indirect
- modernc.org/mathutil v1.5.0 // indirect
- modernc.org/memory v1.5.0 // indirect
- modernc.org/opt v0.1.3 // indirect
- modernc.org/sqlite v1.24.0 // indirect
- modernc.org/strutil v1.1.3 // indirect
- modernc.org/token v1.1.0 // indirect
+ modernc.org/libc v1.62.1 // indirect
+ modernc.org/mathutil v1.7.1 // 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
+replace modernc.org/sqlite v1.33.0 => modernc.org/sqlite v1.32.0
diff --git a/go.sum b/go.sum
index 02fdecbf320..2436125b128 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g=
+cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -29,28 +31,96 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
+cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
-cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
+cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
+cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
+cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
+cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
+cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
+cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
+cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
+cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o=
+cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE=
+cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM=
+cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ=
cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
+cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg=
+cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ=
+cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k=
+cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw=
cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=
+cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M=
+cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE=
+cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE=
+cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk=
+cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc=
+cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8=
+cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc=
+cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04=
+cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8=
+cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY=
+cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM=
+cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc=
+cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU=
+cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI=
+cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8=
+cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno=
+cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak=
+cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84=
+cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A=
+cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E=
cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=
cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=
+cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY=
+cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k=
cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=
cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk=
+cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0=
+cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc=
+cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI=
+cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ=
+cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI=
+cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08=
cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=
cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s=
cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0=
+cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ=
+cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY=
+cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo=
+cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg=
+cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw=
cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=
cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw=
cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI=
+cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
+cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
+cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
+cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
+cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
+cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
+cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=
+cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8=
+cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM=
+cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU=
+cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc=
+cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI=
+cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss=
+cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE=
+cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE=
+cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g=
+cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4=
+cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8=
+cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM=
+cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -58,12 +128,44 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=
+cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw=
+cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc=
+cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E=
+cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac=
+cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q=
+cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU=
cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=
+cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI=
+cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y=
+cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss=
+cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc=
cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=
cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI=
+cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0=
+cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk=
+cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q=
+cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg=
+cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590=
+cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8=
+cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk=
+cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk=
+cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE=
+cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU=
+cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U=
+cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA=
+cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M=
+cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg=
+cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s=
+cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM=
+cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk=
+cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA=
cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=
cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=
+cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4=
+cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI=
+cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y=
+cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
@@ -71,103 +173,369 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
-cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
+cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
+cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
+cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=
+cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
+cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA=
+cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
+cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
-cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
+cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
+cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
+cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
+cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg=
+cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo=
+cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4=
+cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM=
+cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA=
cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=
+cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI=
+cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s=
cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=
cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=
cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc=
+cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE=
+cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM=
+cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M=
+cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0=
+cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8=
cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=
cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=
+cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE=
cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=
cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE=
+cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0=
+cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA=
+cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE=
+cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38=
+cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w=
+cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8=
cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=
cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=
+cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM=
+cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA=
+cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A=
+cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ=
+cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs=
+cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s=
+cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI=
+cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4=
cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=
cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=
+cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM=
+cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c=
cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=
cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ=
+cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g=
+cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4=
+cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs=
+cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww=
+cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c=
+cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s=
+cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI=
+cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ=
cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=
cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0=
cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8=
+cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek=
+cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0=
+cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM=
+cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4=
+cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE=
+cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM=
+cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q=
+cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4=
cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=
cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU=
+cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k=
+cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4=
+cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM=
+cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs=
cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=
cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=
+cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE=
cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=
cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=
+cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc=
+cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY=
+cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU=
+cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI=
+cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8=
+cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M=
+cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc=
+cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw=
+cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw=
+cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY=
+cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w=
+cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI=
+cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs=
+cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
+cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=
cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=
+cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY=
+cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08=
+cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw=
+cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA=
+cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c=
cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=
cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA=
+cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w=
+cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM=
+cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0=
+cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60=
+cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo=
+cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg=
cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=
cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=
+cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw=
cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=
cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=
+cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E=
+cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw=
+cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA=
+cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI=
+cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y=
cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
+cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM=
+cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o=
+cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo=
+cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc=
-cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
+cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
+cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
+cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE=
+cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY=
+cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY=
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
+cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
+cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=
+cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc=
+cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A=
+cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk=
+cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo=
+cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74=
+cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM=
+cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY=
+cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4=
+cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs=
+cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g=
+cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o=
+cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE=
+cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=
+cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg=
+cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0=
+cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg=
+cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w=
+cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24=
+cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI=
cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
+cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE=
+cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8=
+cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY=
cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=
-cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
+cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo=
+cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw=
+cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
+cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk=
+cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM=
+cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE=
+cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
+cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
+cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=
+cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
+cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE=
+cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM=
+cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA=
+cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI=
+cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw=
+cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY=
cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=
cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=
+cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I=
cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=
cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM=
+cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA=
+cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY=
+cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM=
cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=
cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s=
+cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8=
+cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI=
+cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo=
+cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk=
+cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4=
+cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w=
+cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw=
+cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU=
+cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU=
cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=
cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=
+cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM=
+cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8=
+cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E=
+cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM=
+cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8=
+cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4=
+cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY=
cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=
cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=
+cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k=
+cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU=
cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=
cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34=
+cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA=
+cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0=
+cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE=
+cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ=
+cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4=
+cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs=
+cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI=
+cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA=
+cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk=
+cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ=
+cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE=
+cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc=
+cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc=
cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=
cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg=
+cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo=
+cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw=
+cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw=
cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=
cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU=
+cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70=
+cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo=
+cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs=
cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=
cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=
+cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk=
+cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg=
+cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE=
+cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw=
+cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc=
cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=
cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=
+cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg=
+cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI=
+cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0=
+cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8=
+cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4=
+cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg=
+cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k=
+cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM=
cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=
cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=
cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk=
cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo=
+cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE=
+cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U=
+cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA=
+cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c=
cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=
cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=
+cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac=
cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=
cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c=
+cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs=
+cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70=
+cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ=
cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=
cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A=
+cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA=
+cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM=
+cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ=
+cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA=
+cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0=
+cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots=
+cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo=
+cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI=
+cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU=
+cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg=
+cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA=
cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=
cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY=
+cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc=
+cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y=
+cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14=
+cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do=
+cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo=
+cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM=
+cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg=
cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=
cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI=
+cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk=
+cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44=
+cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc=
+cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc=
cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=
+cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4=
+cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4=
+cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU=
cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=
cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=
cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=
+cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q=
+cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA=
+cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8=
+cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0=
cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=
cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc=
+cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk=
+cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk=
+cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0=
+cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag=
+cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU=
+cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s=
+cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA=
+cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc=
+cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk=
cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=
cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg=
+cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4=
+cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U=
+cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY=
+cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s=
+cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco=
+cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo=
+cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc=
+cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4=
+cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E=
+cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU=
+cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec=
+cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA=
+cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4=
+cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw=
+cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A=
+cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos=
+cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk=
+cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M=
cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=
cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=
+cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0=
+cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco=
+cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0=
+cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
@@ -177,98 +545,207 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
-cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
+cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
+cloud.google.com/go/storage v1.49.0 h1:zenOPBOWHCnojRd9aJZAyQXBYqkJkdQS42dxL55CIMw=
+cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5PiwehZI9qGU=
+cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w=
+cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I=
+cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4=
+cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw=
cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=
cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=
+cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM=
+cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA=
+cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c=
+cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8=
+cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4=
+cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc=
+cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ=
+cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg=
+cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM=
+cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28=
+cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y=
+cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA=
+cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk=
+cloud.google.com/go/trace v1.11.2 h1:4ZmaBdL8Ng/ajrgKqY5jfvzqMXbrDcBsUGXOT9aqTtI=
+cloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io=
+cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs=
+cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg=
+cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0=
+cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos=
+cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos=
+cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk=
+cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw=
+cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg=
+cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk=
+cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ=
+cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ=
cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=
cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4=
+cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M=
+cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU=
+cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU=
cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=
cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=
cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo=
+cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY=
+cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E=
+cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY=
+cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0=
+cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE=
+cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g=
+cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc=
+cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY=
+cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208=
+cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8=
+cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY=
+cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w=
+cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8=
+cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes=
cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=
cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=
+cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc=
+cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A=
+cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg=
+cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo=
+cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ=
+cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng=
cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
+cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M=
+cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA=
+cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw=
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
+git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA=
+github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+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.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+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.7.1 h1:5w1SxjGm9MTMNTuRbEPyw21ObdbaagTWF/KfF0qHTRE=
-github.com/CycloneDX/cyclonedx-go v0.7.1/go.mod h1:N/nrdWQI2SIjaACyyDs/u7+ddCkyl/zkNs8xFsHF2Ps=
+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=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
-github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
-github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
+github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
+github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1/go.mod h1:0wEl7vrAD8mehJyohS9HZy+WyEOaQO2mJx86Cvh93kM=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE=
+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.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
-github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
-github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
-github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+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=
-github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
-github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
+github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
-github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
+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.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=
github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY=
-github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
-github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
-github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
-github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
+github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
+github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
+github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
+github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
+github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/anchore/bubbly v0.0.0-20230712165553-812110ab0a10 h1:Wrqt9fd8ygEMyFtxncZU7RgW2qBu5CL1x876xIyUlPU=
-github.com/anchore/bubbly v0.0.0-20230712165553-812110ab0a10/go.mod h1:Ger02eh5NpPm2IqkPAy396HU1KlK3BhOeCljDYXySSk=
-github.com/anchore/clio v0.0.0-20230630162005-9535e9dc2817 h1:YsE91GT81FQOAOKByAnJVeJY2q8AunJ1eNf1bDC/o8g=
-github.com/anchore/clio v0.0.0-20230630162005-9535e9dc2817/go.mod h1:H5f7dtqPQ6kbL0OHcLrc5N0zkIxLZPBL2oKUE03fLgA=
-github.com/anchore/fangs v0.0.0-20230628163043-a51c5a39b097 h1:79jSyWO6WOV8HPEpOQBOr7WsC2DnBRpyl7zsdaahCcg=
-github.com/anchore/fangs v0.0.0-20230628163043-a51c5a39b097/go.mod h1:E3zNHEz7mizIFGJhuX+Ga7AbCmEN5TfzVDxmOfj7XZw=
-github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe h1:Df867YMmymdMG6z5IW8pR0/2CRpLIjYnaTXLp6j+s0k=
-github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
+github.com/anchore/archiver/v3 v3.5.3-0.20241210171143-5b1d8d1c7c51 h1:yhk+P8lF3ZiROjmaVRao9WGTRo4b/wYjoKEiAHWrKwc=
+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-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-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.20230104203445-02e0a6721501 h1:AV7qjwMcM4r8wFhJq3jLRztew3ywIyPTRapl2T1s9o8=
-github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4=
-github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 h1:vrf2PYH77vqVJoNR15ZuFJ63qwBMqrmGIt/7VsBhLF8=
-github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963/go.mod h1:AVRyXOUP0hTz9Cb8OlD1XnwA8t4lBPfTuwPHmEUuiLc=
-github.com/anchore/stereoscope v0.0.0-20230627195312-cd49355d934e h1:zhk3ZLtomMJ750nNCE+c24PonMzoO/SeL/4uTr1L9kM=
-github.com/anchore/stereoscope v0.0.0-20230627195312-cd49355d934e/go.mod h1:0LsgHgXO4QFnk2hsYwtqd3fR18PIZXlFLIl2qb9tu3g=
-github.com/anchore/syft v0.85.0 h1:JShy/YIqffcIR3cvssABGr/yNDRCgZwpcQPcRLO2nHc=
-github.com/anchore/syft v0.85.0/go.mod h1:nCMEh98C1BEfkH49HXKeJNPcUEfDM4B6xmptGT5Lv3Q=
+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.1.4 h1:e+iT9UdUzLBabWGe84hn5sTHDRioY+4IHsVzJXuJlek=
+github.com/anchore/stereoscope v0.1.4/go.mod h1:omWgXDEp/XfqCJlZXIByEo1c3ArZg/qTJ5LBKVLAIdw=
+github.com/anchore/syft v1.26.1 h1:v64AqoYzPGZols2O20xquE93bJr0EH3VqAKsB0fr0WA=
+github.com/anchore/syft v1.26.1/go.mod h1:OyN/wK6s7lmV2kAUm4zcQbZhLznToASL2r+iRQHcIKY=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
-github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
-github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+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=
-github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
+github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
+github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
+github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
+github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
+github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
+github.com/aquasecurity/go-pep440-version v0.0.1 h1:8VKKQtH2aV61+0hovZS3T//rUF+6GDn18paFTVS0h0M=
+github.com/aquasecurity/go-pep440-version v0.0.1/go.mod h1:3naPe+Bp6wi3n4l5iBFCZgS0JG8vY6FT0H4NGhFJ+i4=
+github.com/aquasecurity/go-version v0.0.1 h1:4cNl516agK0TCn5F7mmYN+xVs1E3S45LkgZk3cbaW2E=
+github.com/aquasecurity/go-version v0.0.1/go.mod h1:s1UU6/v2hctXcOa3OLwfj5d9yoXHa3ahf+ipSwEvGT0=
+github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
+github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
-github.com/aws/aws-sdk-go v1.44.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI=
-github.com/aws/aws-sdk-go v1.44.180/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.288 h1:Ln7fIao/nl0ACtelgR1I4AiEw/GLNkKcXfCaHupUW5Q=
+github.com/aws/aws-sdk-go v1.44.288/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -277,81 +754,150 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
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.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
-github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+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=
-github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
+github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
+github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
-github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
-github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
-github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
+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.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 v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
-github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
+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=
+github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
+github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
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.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
-github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
-github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
+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=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
-github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
-github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg=
-github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc=
-github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
-github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
+github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
+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.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=
+github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+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.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=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/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.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=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M=
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
-github.com/docker/cli v23.0.5+incompatible h1:ufWmAOuD3Vmr7JP2G5K3cyuNC4YZWiAsuDEvFVVDafE=
-github.com/docker/cli v23.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
-github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v24.0.4+incompatible h1:s/LVDftw9hjblvqIeTiGYXBCD95nOEEl7qRsRrIOuQI=
-github.com/docker/docker v24.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
-github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
-github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
-github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+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 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.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.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
+github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
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/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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+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/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
+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=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -364,8 +910,19 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
+github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=
+github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q=
+github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
+github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
+github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
+github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
+github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
+github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
+github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk=
github.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ=
github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4=
@@ -374,58 +931,94 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
-github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
-github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
+github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+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.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
-github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+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.1.2 h1:p+Tv0yMgcuO0/vnMe9Qh4tmUgYhI6AsLVlakZ/Sx+DM=
-github.com/github/go-spdx/v2 v2.1.2/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w=
-github.com/gkampitakis/ciinfo v0.2.4 h1:Ip1hf4K7ISRuVlDrheuhaeffg1VOhlyeFGaQ/vTxrtE=
-github.com/gkampitakis/ciinfo v0.2.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
+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.4.8 h1:B/CJswqJ9LwQMI0tiU7ztWK8qlnz6HxOqZm+XIFuEDU=
-github.com/gkampitakis/go-snaps v0.4.8/go.mod h1:8HW4KX3JKV8M0GSw69CvT+Jqhd1AlBPMPpBfjBI3bdY=
-github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
-github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
+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.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=
+github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
+github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
+github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
+github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
+github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
+github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
-github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
-github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
-github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
-github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE=
-github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
+github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
+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.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=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
+github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
+github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc=
github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
-github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+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=
+github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg=
+github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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=
+github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
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=
@@ -452,14 +1045,13 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -474,10 +1066,12 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-containerregistry v0.15.2 h1:MMkSh+tjSdnmJZO7ljvEqV1DjfekB6VUEAZgy3a+TQE=
-github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-containerregistry v0.20.5 h1:4RnlYcDs5hoA++CeFjlbZ/U9Yp1EuWr+UhhTyYQjOP0=
+github.com/google/go-containerregistry v0.20.5/go.mod h1:Q14vdOOzug02bwnhMkZKD4e30pDaD9W65qzXpyzF49E=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
@@ -486,7 +1080,9 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
-github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
+github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
+github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -503,20 +1099,23 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
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-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
-github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
+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.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE=
-github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
-github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
+github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -526,14 +1125,21 @@ github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
-github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc=
-github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
+github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
+github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
+github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
+github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
-github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE=
-github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE=
+github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
+github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
@@ -545,8 +1151,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-github.com/hashicorp/go-getter v1.7.1 h1:SWiSWN/42qdpR0MdhaOc/bLR48PLuP1ZQtYLRlM69uY=
-github.com/hashicorp/go-getter v1.7.1/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744=
+github.com/hashicorp/go-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY=
+github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -564,13 +1170,17 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
+github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
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=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
@@ -578,32 +1188,33 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
-github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
+github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
+github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
-github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
-github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
+github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
-github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
-github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
+github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
+github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -611,87 +1222,98 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y=
+github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+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.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
-github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+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/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
-github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
+github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg=
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.0.0-20230301153543-ba94b245509b h1:boYyvL3tbUuKcMN029mpCl7oYYJ7yIXujLj+fiW4Alc=
-github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b/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=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
+github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
+github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
+github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
+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.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
-github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
-github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
-github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
+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=
+github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
+github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08 h1:AevUBW4cc99rAF8q8vmddIP8qd/0J5s/UyltGbp66dg=
+github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08/go.mod h1:JOkBRrE1HvgTyjk6diFtNGgr8XJMtIfiBzkL5krqzVk=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
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.4/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=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
-github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
-github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
+github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
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/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
-github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
-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.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
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=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -699,54 +1321,76 @@ 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/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.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
+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.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=
+github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
-github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
-github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
-github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
-github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
-github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
-github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
-github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
-github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+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/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
-github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
-github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
-github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
+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-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
-github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
-github.com/owenrumney/go-sarif v1.1.1 h1:QNObu6YX1igyFKhdzd7vgzmw7XsWN3/6NMGuDzBgXmE=
-github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U=
+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=
+github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
+github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ=
+github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
+github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 h1:FvA4bwjKpPqik5WsQ8+4z4DKWgA1tO1RTTtNKr5oYNA=
+github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554/go.mod h1:n73K/hcuJ50MiVznXyN4rde6fZY7naGKWBXOLFTyc94=
+github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
+github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
+github.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITGI=
+github.com/pandatix/go-cvss v0.6.2/go.mod h1:jDXYlQBZrc8nvrMUVVvTG8PhmuShOnKrxP53nOFkt8Q=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=
@@ -754,13 +1398,16 @@ 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.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
-github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
-github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
+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/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
-github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+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=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -770,8 +1417,11 @@ github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -781,71 +1431,97 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
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.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=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+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.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/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
-github.com/sassoftware/go-rpmutils v0.2.0 h1:pKW0HDYMFWQ5b4JQPiI3WI12hGsVoW0V8+GMoZiI/JE=
-github.com/sassoftware/go-rpmutils v0.2.0/go.mod h1:TJJQYtLe/BeEmEjelI3b7xNZjzAukEkeWKmoakvaOoI=
+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=
+github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
+github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
+github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
+github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
+github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ=
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs=
+github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
+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/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.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
-github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
-github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
-github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE=
-github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
+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.2 h1:dtMNjJreWPe37584ajk7m/rQtfJaLpRMk7pUGgvekOg=
-github.com/spdx/tools-golang v0.5.2/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
+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.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
-github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
+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.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
-github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
+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.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
-github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+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.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
-github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
+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=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -856,20 +1532,24 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
-github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
-github.com/sylabs/sif/v2 v2.8.1 h1:whr4Vz12RXfLnYyVGHoD/rD/hbF2g9OW7BJHa+WIqW8=
-github.com/sylabs/sif/v2 v2.8.1/go.mod h1:LQOdYXC9a8i7BleTKRw9lohi0rTbXkJOeS9u0ebvgyM=
-github.com/sylabs/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ=
-github.com/sylabs/squashfs v0.6.1/go.mod h1:ZwpbPCj0ocIvMy2br6KZmix6Gzh6fsGQcCnydMF+Kx8=
+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.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=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
-github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@@ -879,40 +1559,52 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
-github.com/vbatts/go-mtree v0.5.3 h1:S/jYlfG8rZ+a0bhZd+RANXejy7M4Js8fq9U+XoWTd5w=
-github.com/vbatts/go-mtree v0.5.3/go.mod h1:eXsdoPMdL2jcJx6HweWi9lYQxBsTp4lNhqqAjgkZUg8=
-github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
-github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
-github.com/vifraa/gopom v0.2.1 h1:MYVMAMyiGzXPPy10EwojzKIL670kl5Zbae+o3fFvQEM=
-github.com/vifraa/gopom v0.2.1/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=
-github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
-github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
+github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/vbatts/go-mtree v0.5.4 h1:OMAb8jaCyiFA7zXj0Zc/oARcxBDBoeu2LizjB8BVJl0=
+github.com/vbatts/go-mtree v0.5.4/go.mod h1:5GqJbVhm9BBiCc4K5uc/c42FPgXulHaQs4sFUEfIWMo=
+github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
+github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
+github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=
+github.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=
+github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA=
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20=
github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b h1:uWNQ0khA6RdFzODOMwKo9XXu7fuewnnkHykUtuKru8s=
github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b/go.mod h1:ewlIKbKV8l+jCj8rkdXIs361ocR5x3qGyoCSca47Gx8=
-github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 h1:lwgTsTy18nYqASnH58qyfRW/ldj7Gt2zzBvgYPzdA4s=
-github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
-github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
-github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
+github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8=
+github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
+github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
+github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
-github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
-github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0=
-github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
+github.com/zclconf/go-cty v1.14.0 h1:/Xrd39K7DXbHzlisFP9c4pHao4yyf+/Ug9LEz+Y/yhc=
+github.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
+github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
+github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 h1:V+UsotZpAVvfj3X/LMoEytoLzSiP6Lg0F7wdVyu9gGg=
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1/go.mod h1:ly2RBz4mnz1yeuVbQA/VFwGjK3mnHGRj1JuoG336Bis=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
@@ -927,13 +1619,44 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/detectors/gcp v1.29.0 h1:TiaiXB4DpGD3sdzNlYQxruQngn5Apwzi1X0DRhuGvDQ=
+go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
+go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
+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/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
-go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
-go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
+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.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=
@@ -943,31 +1666,47 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
-golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
-golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+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.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=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
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-20230202163644-54bba9f4231b h1:EqBVA+nNsObCwQoBEHy4wLU0pi7i8a4AL3pbItPdPkE=
-golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+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=
+golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -992,13 +1731,18 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1023,7 +1767,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@@ -1040,6 +1783,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -1051,13 +1795,23 @@ golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
-golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
-golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+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.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=
@@ -1082,10 +1836,14 @@ golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7Lm
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
-golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
-golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
+golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
+golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1099,13 +1857,18 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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.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=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1118,14 +1881,11 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1148,12 +1908,12 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1168,11 +1928,13 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1181,8 +1943,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1194,24 +1956,38 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+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.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=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
-golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
-golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+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.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=
@@ -1223,17 +1999,30 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+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.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=
-golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
-golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -1247,8 +2036,8 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1280,7 +2069,6 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -1290,10 +2078,15 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
-golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
+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.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=
@@ -1301,8 +2094,17 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
+gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
+gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
+gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -1352,16 +2154,24 @@ google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaE
google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
+google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08=
google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
-google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es=
-google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
+google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
+google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
+google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
+google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
+google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
+google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
+google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
+google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
+google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0=
+google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -1437,6 +2247,7 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
+google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
@@ -1469,9 +2280,41 @@ google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53B
google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=
google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
-google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
-google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
+google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
+google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
+google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE=
+google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
+google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
+google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
+google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA=
+google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
+google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
+google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
+google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
+google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
+google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
+google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
+google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
+google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1508,8 +2351,13 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
-google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
-google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
+google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
+google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
+google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
+google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
+google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1526,8 +2374,11 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+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.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=
@@ -1536,12 +2387,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
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=
@@ -1550,17 +2396,16 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
-gorm.io/gorm v1.23.10 h1:4Ne9ZbzID9GUxRkllxN4WjJKpsHx8YbKvekVdgyWh24=
-gorm.io/gorm v1.23.10/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
+gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
+gorm.io/gorm v1.30.0/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=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1568,53 +2413,67 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
-lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
-lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
-modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
-modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
+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.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.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
-modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
-modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
+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.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/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
+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.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=
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
-modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
-modernc.org/libc v1.16.8/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
-modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
-modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
+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.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 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
-modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
-modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+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.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 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k=
-modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI=
-modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+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.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 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
-modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
-modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/grype/cpe/cpe.go b/grype/cpe/cpe.go
index fa058d610c6..6f660e10337 100644
--- a/grype/cpe/cpe.go
+++ b/grype/cpe/cpe.go
@@ -1,6 +1,8 @@
package cpe
import (
+ "github.com/facebookincubator/nvdtools/wfn"
+
"github.com/anchore/grype/internal/log"
"github.com/anchore/syft/syft/cpe"
)
@@ -8,7 +10,7 @@ import (
func NewSlice(cpeStrs ...string) ([]cpe.CPE, error) {
var cpes []cpe.CPE
for _, c := range cpeStrs {
- value, err := cpe.New(c)
+ value, err := cpe.New(c, "")
if err != nil {
log.Warnf("excluding invalid CPE %q: %v", c, err)
continue
@@ -21,9 +23,12 @@ func NewSlice(cpeStrs ...string) ([]cpe.CPE, error) {
func MatchWithoutVersion(c cpe.CPE, candidates []cpe.CPE) []cpe.CPE {
matches := make([]cpe.CPE, 0)
+ a := wfn.Attributes(c.Attributes)
+ a.Update = wfn.Any
for _, candidate := range candidates {
- canCopy := candidate
- if c.MatchWithoutVersion(&canCopy) {
+ canCopy := wfn.Attributes(candidate.Attributes)
+ canCopy.Update = wfn.Any
+ if a.MatchWithoutVersion(&canCopy) {
matches = append(matches, candidate)
}
}
diff --git a/grype/cpe/cpe_test.go b/grype/cpe/cpe_test.go
index df1b57eb2ae..f259ee097df 100644
--- a/grype/cpe/cpe_test.go
+++ b/grype/cpe/cpe_test.go
@@ -17,88 +17,88 @@ func TestMatchWithoutVersion(t *testing.T) {
}{
{
name: "GoCase",
- compare: cpe.Must("cpe:2.3:*:python-requests:requests:2.3.0:*:*:*:*:python:*:*"),
+ compare: cpe.Must("cpe:2.3:*:python-requests:requests:2.3.0:*:*:*:*:python:*:*", ""),
candidates: []cpe.CPE{
- cpe.Must("cpe:2.3:a:python-requests:requests:2.2.1:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:python-requests:requests:2.2.1:*:*:*:*:*:*:*", ""),
},
expected: []cpe.CPE{
- cpe.Must("cpe:2.3:a:python-requests:requests:2.2.1:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:python-requests:requests:2.2.1:*:*:*:*:*:*:*", ""),
},
},
{
name: "IgnoreVersion",
- compare: cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*"),
+ compare: cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""),
candidates: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name:3.3:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name:5.5:*:*:*:*:java:*:*"),
+ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name:3.3:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name:5.5:*:*:*:*:java:*:*", ""),
},
expected: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name:3.3:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name:5.5:*:*:*:*:java:*:*"),
+ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name:3.3:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name:5.5:*:*:*:*:java:*:*", ""),
},
},
{
name: "MatchByTargetSW",
- compare: cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*"),
+ compare: cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""),
candidates: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:maven:*:*"),
- cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:jenkins:*:*"),
- cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*"),
- cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:maven:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:jenkins:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*", ""),
},
expected: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*", ""),
},
},
{
name: "MatchByName",
- compare: cpe.Must("cpe:2.3:*:name:name5:3.2:*:*:*:*:java:*:*"),
+ compare: cpe.Must("cpe:2.3:*:name:name5:3.2:*:*:*:*:java:*:*", ""),
candidates: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name:name1:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name2:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name3:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name4:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name:name5:3.2:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name:name1:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name2:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name3:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name4:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name:name5:3.2:*:*:*:*:*:*:*", ""),
},
expected: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name:name5:3.2:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name:name5:3.2:*:*:*:*:*:*:*", ""),
},
},
{
name: "MatchByVendor",
- compare: cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*"),
+ compare: cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""),
candidates: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*"),
- cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*", ""),
+ cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*", ""),
},
expected: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*"),
+ cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""),
},
},
{
name: "MatchAnyVendorOrTargetSW",
- compare: cpe.Must("cpe:2.3:*:*:name:3.2:*:*:*:*:*:*:*"),
+ compare: cpe.Must("cpe:2.3:*:*:name:3.2:*:*:*:*:*:*:*", ""),
candidates: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*"),
- cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*"),
- cpe.Must("cpe:2.3:*:name5:NOMATCH:3.2:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*", ""),
+ cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*", ""),
+ cpe.Must("cpe:2.3:*:name5:NOMATCH:3.2:*:*:*:*:*:*:*", ""),
},
expected: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*"),
- cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*", ""),
+ cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*", ""),
},
},
}
@@ -109,17 +109,17 @@ func TestMatchWithoutVersion(t *testing.T) {
if len(actual) != len(test.expected) {
for _, e := range actual {
- t.Errorf(" unexpected entry: %+v", e.BindToFmtString())
+ t.Errorf(" unexpected entry: %+v", e.Attributes.BindToFmtString())
}
t.Fatalf("unexpected number of entries: %d", len(actual))
}
for idx, a := range actual {
e := test.expected[idx]
- if a.BindToFmtString() != e.BindToFmtString() {
+ if a.Attributes.BindToFmtString() != e.Attributes.BindToFmtString() {
dmp := diffmatchpatch.New()
- diffs := dmp.DiffMain(a.BindToFmtString(), e.BindToFmtString(), true)
- t.Errorf("mismatched entries @ %d:\n\texpected:%+v\n\t actual:%+v\n\t diff:%+v\n", idx, e.BindToFmtString(), a.BindToFmtString(), dmp.DiffPrettyText(diffs))
+ diffs := dmp.DiffMain(a.Attributes.BindToFmtString(), e.Attributes.BindToFmtString(), true)
+ t.Errorf("mismatched entries @ %d:\n\texpected:%+v\n\t actual:%+v\n\t diff:%+v\n", idx, e.Attributes.BindToFmtString(), a.Attributes.BindToFmtString(), dmp.DiffPrettyText(diffs))
}
}
})
diff --git a/grype/db/curator_test.go b/grype/db/curator_test.go
deleted file mode 100644
index bb3ee22ac09..00000000000
--- a/grype/db/curator_test.go
+++ /dev/null
@@ -1,366 +0,0 @@
-package db
-
-import (
- "bufio"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "strconv"
- "syscall"
- "testing"
- "time"
-
- "github.com/gookit/color"
- "github.com/spf13/afero"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/wagoodman/go-progress"
-
- "github.com/anchore/grype/internal/file"
- "github.com/anchore/grype/internal/stringutil"
-)
-
-type testGetter struct {
- file map[string]string
- dir map[string]string
- calls stringutil.StringSet
- fs afero.Fs
-}
-
-func newTestGetter(fs afero.Fs, f, d map[string]string) *testGetter {
- return &testGetter{
- file: f,
- dir: d,
- calls: stringutil.NewStringSet(),
- fs: fs,
- }
-}
-
-// GetFile downloads the give URL into the given path. The URL must reference a single file.
-func (g *testGetter) GetFile(dst, src string, _ ...*progress.Manual) error {
- g.calls.Add(src)
- if _, ok := g.file[src]; !ok {
- return fmt.Errorf("blerg, no file!")
- }
- return afero.WriteFile(g.fs, dst, []byte(g.file[src]), 0755)
-}
-
-// Get downloads the given URL into the given directory. The directory must already exist.
-func (g *testGetter) GetToDir(dst, src string, _ ...*progress.Manual) error {
- g.calls.Add(src)
- if _, ok := g.dir[src]; !ok {
- return fmt.Errorf("blerg, no file!")
- }
- return afero.WriteFile(g.fs, dst, []byte(g.dir[src]), 0755)
-}
-
-func newTestCurator(tb testing.TB, fs afero.Fs, getter file.Getter, dbDir, metadataUrl string, validateDbHash bool) Curator {
- c, err := NewCurator(Config{
- DBRootDir: dbDir,
- ListingURL: metadataUrl,
- ValidateByHashOnGet: validateDbHash,
- })
-
- require.NoError(tb, err)
-
- c.downloader = getter
- c.fs = fs
-
- return c
-}
-
-func Test_defaultHTTPClient(t *testing.T) {
- tests := []struct {
- name string
- hasCert bool
- }{
- {
- name: "no custom cert should use default system root certs",
- hasCert: false,
- },
- {
- name: "should use single custom cert",
- hasCert: true,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- var certPath string
- if test.hasCert {
- certPath = generateCertFixture(t)
- }
-
- httpClient, err := defaultHTTPClient(afero.NewOsFs(), certPath)
- require.NoError(t, err)
-
- if test.hasCert {
- require.NotNil(t, httpClient.Transport.(*http.Transport).TLSClientConfig)
- assert.Len(t, httpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects(), 1)
- } else {
- assert.Nil(t, httpClient.Transport.(*http.Transport).TLSClientConfig)
- }
-
- })
- }
-}
-
-func generateCertFixture(t *testing.T) string {
- path := "test-fixtures/tls/server.crt"
- if _, err := os.Stat(path); !os.IsNotExist(err) {
- // fixture already exists...
- return path
- }
-
- t.Logf(color.Bold.Sprint("Generating Key/Cert Fixture"))
-
- cwd, err := os.Getwd()
- if err != nil {
- t.Errorf("unable to get cwd: %+v", err)
- }
-
- cmd := exec.Command("make", "server.crt")
- cmd.Dir = filepath.Join(cwd, "test-fixtures/tls")
-
- stderr, err := cmd.StderrPipe()
- if err != nil {
- t.Fatalf("could not get stderr: %+v", err)
- }
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- t.Fatalf("could not get stdout: %+v", err)
- }
-
- err = cmd.Start()
- if err != nil {
- t.Fatalf("failed to start cmd: %+v", err)
- }
-
- show := func(label string, reader io.ReadCloser) {
- scanner := bufio.NewScanner(reader)
- scanner.Split(bufio.ScanLines)
- for scanner.Scan() {
- t.Logf("%s: %s", label, scanner.Text())
- }
- }
- go show("out", stdout)
- go show("err", stderr)
-
- if err := cmd.Wait(); err != nil {
- if exiterr, ok := err.(*exec.ExitError); ok {
- // The program has exited with an exit code != 0
-
- // This works on both Unix and Windows. Although package
- // syscall is generally platform dependent, WaitStatus is
- // defined for both Unix and Windows and in both cases has
- // an ExitStatus() method with the same signature.
- if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
- if status.ExitStatus() != 0 {
- t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
- }
- }
- } else {
- t.Fatalf("unable to get generate fixture result: %+v", err)
- }
- }
- return path
-}
-
-func TestCuratorDownload(t *testing.T) {
- tests := []struct {
- name string
- entry *ListingEntry
- expectedURL string
- err bool
- }{
- {
- name: "download populates returned tempdir",
- entry: &ListingEntry{
- Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
- URL: mustUrl(url.Parse("http://a-url/payload.tar.gz")),
- Checksum: "sha256:deadbeefcafe",
- },
- expectedURL: "http://a-url/payload.tar.gz?checksum=sha256%3Adeadbeefcafe",
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- metadataUrl := "http://metadata.io"
- contents := "CONTENTS!!!"
- files := map[string]string{}
- dirs := map[string]string{
- test.expectedURL: contents,
- }
- fs := afero.NewMemMapFs()
- getter := newTestGetter(fs, files, dirs)
- cur := newTestCurator(t, fs, getter, "/tmp/dbdir", metadataUrl, false)
-
- path, err := cur.download(test.entry, &progress.Manual{})
- if err != nil {
- t.Fatalf("could not download entry: %+v", err)
- }
-
- if !getter.calls.Contains(test.expectedURL) {
- t.Fatalf("never made the appropriate fetch call: %+v", getter.calls)
- }
-
- f, err := fs.Open(path)
- if err != nil {
- t.Fatalf("no db file: %+v", err)
- }
-
- actual, err := afero.ReadAll(f)
- if err != nil {
- t.Fatalf("bad db file read: %+v", err)
- }
-
- if string(actual) != contents {
- t.Fatalf("bad contents: %+v", string(actual))
- }
- })
- }
-}
-
-func TestCuratorValidate(t *testing.T) {
- tests := []struct {
- name string
- fixture string
- constraint int
- cfgValidateDbHash bool
- err bool
- }{
- {
- name: "good checksum & good constraint",
- fixture: "test-fixtures/curator-validate/good-checksum",
- cfgValidateDbHash: true,
- constraint: 1,
- err: false,
- },
- {
- name: "good checksum & bad constraint",
- fixture: "test-fixtures/curator-validate/good-checksum",
- cfgValidateDbHash: true,
- constraint: 2,
- err: true,
- },
- {
- name: "bad checksum & good constraint",
- fixture: "test-fixtures/curator-validate/bad-checksum",
- cfgValidateDbHash: true,
- constraint: 1,
- err: true,
- },
- {
- name: "bad checksum & bad constraint",
- fixture: "test-fixtures/curator-validate/bad-checksum",
- cfgValidateDbHash: true,
- constraint: 2,
- err: true,
- },
- {
- name: "bad checksum ignored on config exception",
- fixture: "test-fixtures/curator-validate/bad-checksum",
- cfgValidateDbHash: false,
- constraint: 1,
- err: false,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- metadataUrl := "http://metadata.io"
-
- fs := afero.NewOsFs()
- getter := newTestGetter(fs, nil, nil)
- cur := newTestCurator(t, fs, getter, "/tmp/dbdir", metadataUrl, test.cfgValidateDbHash)
-
- cur.targetSchema = test.constraint
-
- md, err := cur.validateIntegrity(test.fixture)
-
- if err == nil && test.err {
- t.Errorf("expected an error but got none")
- } else if err != nil && !test.err {
- assert.NotZero(t, md)
- t.Errorf("expected no error, got: %+v", err)
- }
- })
- }
-}
-
-func TestCuratorDBPathHasSchemaVersion(t *testing.T) {
- fs := afero.NewMemMapFs()
- dbRootPath := "/tmp/dbdir"
- cur := newTestCurator(t, fs, nil, dbRootPath, "http://metadata.io", false)
-
- assert.Equal(t, path.Join(dbRootPath, strconv.Itoa(cur.targetSchema)), cur.dbDir, "unexpected dir")
- assert.Contains(t, cur.dbPath, path.Join(dbRootPath, strconv.Itoa(cur.targetSchema)), "unexpected path")
-}
-
-func TestCurator_validateStaleness(t *testing.T) {
- type fields struct {
- validateAge bool
- maxAllowedDBAge time.Duration
- md Metadata
- }
-
- now := time.Now().UTC()
- tests := []struct {
- name string
- cur *Curator
- fields fields
- wantErr assert.ErrorAssertionFunc
- }{
- {
- name: "no-validation",
- fields: fields{
- md: Metadata{Built: now},
- },
- wantErr: assert.NoError,
- },
- {
- name: "up-to-date",
- fields: fields{
- maxAllowedDBAge: 2 * time.Hour,
- validateAge: true,
- md: Metadata{Built: now},
- },
- wantErr: assert.NoError,
- },
- {
- name: "stale-data",
- fields: fields{
- maxAllowedDBAge: time.Hour,
- validateAge: true,
- md: Metadata{Built: now.UTC().Add(-4 * time.Hour)},
- },
- wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
- return assert.ErrorContains(t, err, "the vulnerability database was built")
- },
- },
- {
- name: "stale-data-no-validation",
- fields: fields{
- maxAllowedDBAge: time.Hour,
- validateAge: false,
- md: Metadata{Built: now.Add(-4 * time.Hour)},
- },
- wantErr: assert.NoError,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- c := &Curator{
- validateAge: tt.fields.validateAge,
- maxAllowedBuiltAge: tt.fields.maxAllowedDBAge,
- }
- tt.wantErr(t, c.validateStaleness(tt.fields.md), fmt.Sprintf("validateStaleness(%v)", tt.fields.md))
- })
- }
-}
diff --git a/grype/db/db_closer.go b/grype/db/db_closer.go
deleted file mode 100644
index 0ff48538ccd..00000000000
--- a/grype/db/db_closer.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package db
-
-import v5 "github.com/anchore/grype/grype/db/v5"
-
-// Closer lets receiver close the db connection and free any allocated db resources.
-// It's especially useful if vulnerability DB loaded repeatedly during some periodic SBOM scanning process.
-type Closer struct {
- v5.DBCloser
-}
diff --git a/grype/db/internal/gormadapter/logger.go b/grype/db/internal/gormadapter/logger.go
index fd285ac9f23..9dcbf12ff67 100644
--- a/grype/db/internal/gormadapter/logger.go
+++ b/grype/db/internal/gormadapter/logger.go
@@ -2,36 +2,71 @@ package gormadapter
import (
"context"
+ "fmt"
"time"
"gorm.io/gorm/logger"
+ anchoreLogger "github.com/anchore/go-logger"
"github.com/anchore/grype/internal/log"
)
+// logAdapter is meant to adapt the gorm logger interface (see https://github.com/go-gorm/gorm/blob/v1.25.12/logger/logger.go)
+// to the anchore logger interface.
type logAdapter struct {
+ debug bool
+ slowThreshold time.Duration
+ level logger.LogLevel
}
-func newLogger() logger.Interface {
- return logAdapter{}
+// LogMode sets the log level for the logger and returns a new instance
+func (l *logAdapter) LogMode(level logger.LogLevel) logger.Interface {
+ newlogger := *l
+ newlogger.level = level
+ return &newlogger
}
-func (l logAdapter) LogMode(logger.LogLevel) logger.Interface {
- return l
-}
-
-func (l logAdapter) Info(_ context.Context, _ string, _ ...interface{}) {
- // unimplemented
+func (l logAdapter) Info(_ context.Context, fmt string, v ...interface{}) {
+ if l.level >= logger.Info {
+ if l.debug {
+ log.Infof("[sql] "+fmt, v...)
+ }
+ }
}
func (l logAdapter) Warn(_ context.Context, fmt string, v ...interface{}) {
- log.Warnf("gorm: "+fmt, v...)
+ if l.level >= logger.Warn {
+ log.Warnf("[sql] "+fmt, v...)
+ }
}
func (l logAdapter) Error(_ context.Context, fmt string, v ...interface{}) {
- log.Errorf("gorm: "+fmt, v...)
+ if l.level >= logger.Error {
+ log.Errorf("[sql] "+fmt, v...)
+ }
}
-func (l logAdapter) Trace(_ context.Context, _ time.Time, _ func() (sql string, rowsAffected int64), _ error) {
- // unimplemented
+// Trace logs the SQL statement and the duration it took to run the statement
+func (l logAdapter) Trace(_ context.Context, t time.Time, fn func() (sql string, rowsAffected int64), _ error) {
+ if l.level <= logger.Silent {
+ return
+ }
+
+ if l.debug {
+ sql, rowsAffected := fn()
+ elapsed := time.Since(t)
+ fields := anchoreLogger.Fields{
+ "rows": rowsAffected,
+ "duration": elapsed,
+ }
+
+ isSlow := l.slowThreshold != 0 && elapsed > l.slowThreshold
+ if isSlow {
+ fields["is-slow"] = isSlow
+ fields["slow-threshold"] = fmt.Sprintf("> %s", l.slowThreshold)
+ log.WithFields(fields).Warnf("[sql] %s", sql)
+ } else {
+ log.WithFields(fields).Tracef("[sql] %s", sql)
+ }
+ }
}
diff --git a/grype/db/internal/gormadapter/open.go b/grype/db/internal/gormadapter/open.go
index 00ad3eefeb7..7952fb5486d 100644
--- a/grype/db/internal/gormadapter/open.go
+++ b/grype/db/internal/gormadapter/open.go
@@ -3,65 +3,280 @@ package gormadapter
import (
"fmt"
"os"
+ "path/filepath"
+ "strings"
+ "time"
+ "github.com/glebarez/sqlite"
"gorm.io/gorm"
- "github.com/anchore/sqlite"
+ "github.com/anchore/grype/internal/log"
)
+var commonStatements = []string{
+ `PRAGMA foreign_keys = ON`, // needed for v6+
+}
+
var writerStatements = []string{
- // performance improvements (note: will result in lost data on write interruptions).
- // on my box it reduces the time to write from 10 minutes to 10 seconds (with ~1GB memory utilization spikes)
- `PRAGMA synchronous = OFF`,
- `PRAGMA journal_mode = MEMORY`,
+ // performance improvements (note: will result in lost data on write interruptions)
+ `PRAGMA synchronous = OFF`, // minimize the amount of syncing to disk, prioritizing write performance over durability
+ `PRAGMA journal_mode = MEMORY`, // do not write the journal to disk (maximizing write performance); OFF is faster but less safe in terms of DB consistency
}
-var readOptions = []string{
- "immutable=1",
- "cache=shared",
- "mode=ro",
+var heavyWriteStatements = []string{
+ `PRAGMA cache_size = -1073741824`, // ~1 GB (negative means treat as bytes not page count); one caveat is to not pick a value that risks swapping behavior, negating performance gains
+ `PRAGMA mmap_size = 1073741824`, // ~1 GB; the maximum size of the memory-mapped I/O buffer (to access the database file as if it were a part of the process’s virtual memory)
+ `PRAGMA defer_foreign_keys = ON`, // defer enforcement of foreign key constraints until the end of the transaction (to avoid the overhead of checking constraints for each row)
}
-// Open a new connection to a sqlite3 database file
-func Open(path string, write bool) (*gorm.DB, error) {
- if write {
- // the file may or may not exist, so we ignore the error explicitly
- _ = os.Remove(path)
+var readConnectionOptions = []string{
+ "immutable=1", // indicates that the database file is guaranteed not to change during the connection’s lifetime (slight performance benefit for read-only cases)
+ "mode=ro", // opens the database in as read-only (an enforcement mechanism to allow immutable=1 to be effective)
+ "cache=shared", // multiple database connections within the same process share a single page cache
+}
+
+type config struct {
+ debug bool
+ path string
+ writable bool
+ truncate bool
+ allowLargeMemoryFootprint bool
+ models []any
+ initialData []any
+ memory bool
+ statements []string
+}
+
+type Option func(*config)
+
+func WithDebug(debug bool) Option {
+ return func(c *config) {
+ c.debug = debug
}
+}
- connStr, err := connectionString(path)
- if err != nil {
- return nil, err
+func WithTruncate(truncate bool, models []any, initialData []any) Option {
+ return func(c *config) {
+ c.truncate = truncate
+ if truncate {
+ c.writable = true
+ c.models = models
+ c.initialData = initialData
+ c.allowLargeMemoryFootprint = true
+ }
}
+}
+
+func WithStatements(statements ...string) Option {
+ return func(c *config) {
+ c.statements = append(c.statements, statements...)
+ }
+}
- if !write {
- // &immutable=1&cache=shared&mode=ro
- for _, o := range readOptions {
- connStr += fmt.Sprintf("&%s", o)
+func WithModels(models []any) Option {
+ return func(c *config) {
+ c.models = append(c.models, models...)
+ }
+}
+
+func WithWritable(write bool, models []any) Option {
+ return func(c *config) {
+ c.writable = write
+ c.models = models
+ }
+}
+
+func WithLargeMemoryFootprint(largeFootprint bool) Option {
+ return func(c *config) {
+ c.allowLargeMemoryFootprint = largeFootprint
+ }
+}
+
+func newConfig(path string, opts []Option) config {
+ c := config{}
+ c.apply(path, opts)
+ return c
+}
+
+func (c *config) apply(path string, opts []Option) {
+ for _, o := range opts {
+ o(c)
+ }
+ c.memory = len(path) == 0
+ c.path = path
+}
+
+func (c config) connectionString() string {
+ var conn string
+ if c.path == "" {
+ conn = ":memory:"
+ } else {
+ conn = fmt.Sprintf("file:%s?cache=shared", c.path)
+ }
+
+ if !c.writable && !c.memory {
+ if !strings.Contains(conn, "?") {
+ conn += "?"
+ }
+ for _, o := range readConnectionOptions {
+ conn += fmt.Sprintf("&%s", o)
}
}
+ return conn
+}
- dbObj, err := gorm.Open(sqlite.Open(connStr), &gorm.Config{Logger: newLogger()})
+// Open a new connection to a sqlite3 database file
+func Open(path string, options ...Option) (*gorm.DB, error) {
+ cfg := newConfig(path, options)
+
+ if cfg.truncate && !cfg.writable {
+ return nil, fmt.Errorf("cannot truncate a read-only DB")
+ }
+
+ if cfg.truncate {
+ if err := deleteDB(path); err != nil {
+ return nil, err
+ }
+ }
+
+ dbObj, err := gorm.Open(sqlite.Open(cfg.connectionString()), &gorm.Config{Logger: &logAdapter{
+ debug: cfg.debug,
+ slowThreshold: 400 * time.Millisecond,
+ }})
if err != nil {
return nil, fmt.Errorf("unable to connect to DB: %w", err)
}
- if write {
- for _, sqlStmt := range writerStatements {
- dbObj.Exec(sqlStmt)
- if dbObj.Error != nil {
- return nil, fmt.Errorf("unable to execute (%s): %w", sqlStmt, dbObj.Error)
+ return cfg.prepareDB(dbObj)
+}
+
+func (c config) prepareDB(dbObj *gorm.DB) (*gorm.DB, error) {
+ if c.writable {
+ log.WithFields("path", c.path).Debug("using writable DB statements")
+ if err := c.applyStatements(dbObj, writerStatements); err != nil {
+ return nil, fmt.Errorf("unable to apply DB writer statements: %w", err)
+ }
+ }
+
+ if c.truncate && c.allowLargeMemoryFootprint {
+ log.WithFields("path", c.path).Debug("using large memory footprint DB statements")
+ if err := c.applyStatements(dbObj, heavyWriteStatements); err != nil {
+ return nil, fmt.Errorf("unable to apply DB heavy writer statements: %w", err)
+ }
+ }
+
+ if len(commonStatements) > 0 {
+ if err := c.applyStatements(dbObj, commonStatements); err != nil {
+ return nil, fmt.Errorf("unable to apply DB common statements: %w", err)
+ }
+ }
+
+ if len(c.statements) > 0 {
+ if err := c.applyStatements(dbObj, c.statements); err != nil {
+ return nil, fmt.Errorf("unable to apply DB custom statements: %w", err)
+ }
+ }
+
+ if len(c.models) > 0 && c.writable {
+ log.WithFields("path", c.path).Debug("applying DB migrations")
+ if err := dbObj.AutoMigrate(c.models...); err != nil {
+ return nil, fmt.Errorf("unable to migrate: %w", err)
+ }
+ // now that there are potentially new models and indexes, analyze the DB to ensure the query planner is up-to-date
+ if err := dbObj.Exec("ANALYZE").Error; err != nil {
+ return nil, fmt.Errorf("unable to analyze DB: %w", err)
+ }
+ }
+
+ if len(c.initialData) > 0 && c.truncate {
+ log.WithFields("path", c.path).Debug("writing initial data")
+ for _, d := range c.initialData {
+ if err := dbObj.Create(d).Error; err != nil {
+ return nil, fmt.Errorf("unable to create initial data: %w", err)
}
}
}
+ if c.debug {
+ dbObj = dbObj.Debug()
+ }
+
return dbObj, nil
}
-// ConnectionString creates a connection string for sqlite3
-func connectionString(path string) (string, error) {
- if path == "" {
- return "", fmt.Errorf("no db filepath given")
+func (c config) applyStatements(db *gorm.DB, statements []string) error {
+ for _, sqlStmt := range statements {
+ if err := db.Exec(sqlStmt).Error; err != nil {
+ return fmt.Errorf("unable to execute (%s): %w", sqlStmt, err)
+ }
+ if strings.HasPrefix(sqlStmt, "PRAGMA") {
+ name, value, err := c.pragmaNameValue(sqlStmt)
+ if err != nil {
+ return fmt.Errorf("unable to parse PRAGMA statement: %w", err)
+ }
+
+ var result string
+ if err := db.Raw("PRAGMA " + name + ";").Scan(&result).Error; err != nil {
+ return fmt.Errorf("unable to verify PRAGMA %q: %w", name, err)
+ }
+
+ if !strings.EqualFold(result, value) {
+ if value == "ON" && result == "1" {
+ continue
+ }
+ if value == "OFF" && result == "0" {
+ continue
+ }
+ return fmt.Errorf("PRAGMA %q was not set to %q (%q)", name, value, result)
+ }
+ }
+ }
+ return nil
+}
+
+func (c config) pragmaNameValue(sqlStmt string) (string, string, error) {
+ sqlStmt = strings.TrimSuffix(strings.TrimSpace(sqlStmt), ";") // remove the trailing semicolon
+ if strings.Count(sqlStmt, ";") > 0 {
+ return "", "", fmt.Errorf("PRAGMA statements should not contain semicolons: %q", sqlStmt)
+ }
+
+ // check if the pragma was set, parse the pragma name and value from the statement. This is because
+ // sqlite will not return errors when there are issues with the pragma key or value, but it will
+ // be inconsistent with the expected value if you explicitly check
+ var name, value string
+
+ clean := strings.TrimPrefix(sqlStmt, "PRAGMA")
+ fields := strings.SplitN(clean, "=", 2)
+ if len(fields) == 2 {
+ name = strings.ToLower(strings.TrimSpace(fields[0]))
+ value = strings.TrimSpace(fields[1])
+ } else {
+ return "", "", fmt.Errorf("unable to parse PRAGMA statement: %q", sqlStmt)
+ }
+
+ if c.memory && name == "mmap_size" {
+ // memory only DBs do not have mmap capability
+ value = ""
}
- return fmt.Sprintf("file:%s?cache=shared", path), nil
+
+ if name == "" {
+ return "", "", fmt.Errorf("unable to parse name from PRAGMA statement: %q", sqlStmt)
+ }
+
+ return name, value, nil
+}
+
+func deleteDB(path string) error {
+ if _, err := os.Stat(path); err == nil {
+ if err := os.Remove(path); err != nil {
+ return fmt.Errorf("unable to remove existing DB file: %w", err)
+ }
+ }
+
+ parent := filepath.Dir(path)
+ if err := os.MkdirAll(parent, 0700); err != nil {
+ return fmt.Errorf("unable to create parent directory %q for DB file: %w", parent, err)
+ }
+
+ return nil
}
diff --git a/grype/db/internal/gormadapter/open_test.go b/grype/db/internal/gormadapter/open_test.go
new file mode 100644
index 00000000000..46d356db597
--- /dev/null
+++ b/grype/db/internal/gormadapter/open_test.go
@@ -0,0 +1,224 @@
+package gormadapter
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestConfigApply(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ options []Option
+ expectedPath string
+ expectedMemory bool
+ }{
+ {
+ name: "apply with path",
+ path: "test.db",
+ options: []Option{},
+ expectedPath: "test.db",
+ expectedMemory: false,
+ },
+ {
+ name: "apply with empty path (memory)",
+ path: "",
+ options: []Option{},
+ expectedPath: "",
+ expectedMemory: true,
+ },
+ {
+ name: "apply with truncate option",
+ path: "test.db",
+ options: []Option{WithTruncate(true, nil, nil)}, // migration and initial data don't matter
+ expectedPath: "test.db",
+ expectedMemory: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := newConfig(tt.path, tt.options)
+
+ require.Equal(t, tt.expectedPath, c.path)
+ require.Equal(t, tt.expectedMemory, c.memory)
+ })
+ }
+}
+
+func TestConfigConnectionString(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ write bool
+ memory bool
+ expectedConnStr string
+ }{
+ {
+ name: "writable path",
+ path: "test.db",
+ write: true,
+ expectedConnStr: "file:test.db?cache=shared",
+ },
+ {
+ name: "read-only path",
+ path: "test.db",
+ write: false,
+ expectedConnStr: "file:test.db?cache=shared&immutable=1&mode=ro&cache=shared",
+ },
+ {
+ name: "in-memory mode",
+ path: "",
+ write: false,
+ memory: true,
+ expectedConnStr: ":memory:",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := config{
+ path: tt.path,
+ writable: tt.write,
+ memory: tt.memory,
+ }
+ require.Equal(t, tt.expectedConnStr, c.connectionString())
+ })
+ }
+}
+
+func TestPrepareWritableDB(t *testing.T) {
+
+ t.Run("creates new directory and file when path does not exist", func(t *testing.T) {
+ tempDir := t.TempDir()
+ dbPath := filepath.Join(tempDir, "newdir", "test.db")
+
+ err := deleteDB(dbPath)
+ require.NoError(t, err)
+
+ _, err = os.Stat(filepath.Dir(dbPath))
+ require.NoError(t, err)
+ })
+
+ t.Run("removes existing file at path", func(t *testing.T) {
+ tempDir := t.TempDir()
+ dbPath := filepath.Join(tempDir, "test.db")
+
+ _, err := os.Create(dbPath)
+ require.NoError(t, err)
+
+ _, err = os.Stat(dbPath)
+ require.NoError(t, err)
+
+ err = deleteDB(dbPath)
+ require.NoError(t, err)
+
+ _, err = os.Stat(dbPath)
+ require.True(t, os.IsNotExist(err))
+ })
+
+ t.Run("returns error if unable to create parent directory", func(t *testing.T) {
+ invalidDir := filepath.Join("/root", "invalidDir", "test.db")
+ err := deleteDB(invalidDir)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unable to create parent directory")
+ })
+}
+
+func TestPragmaNameValue(t *testing.T) {
+ tests := []struct {
+ name string
+ cfg config
+ input string
+ wantName string
+ wantValue string
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "basic pragma",
+ cfg: config{memory: false},
+ input: "PRAGMA journal_mode=WAL",
+ wantName: "journal_mode",
+ wantValue: "WAL",
+ },
+ {
+ name: "pragma with spaces",
+ cfg: config{memory: false},
+ input: "PRAGMA cache_size = 2000 ",
+ wantName: "cache_size",
+ wantValue: "2000",
+ },
+ {
+ name: "pragma with trailing semicolon",
+ cfg: config{memory: false},
+ input: "PRAGMA synchronous=NORMAL;",
+ wantName: "synchronous",
+ wantValue: "NORMAL",
+ },
+ {
+ name: "pragma with multiple semicolons",
+ cfg: config{memory: false},
+ input: "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
+ wantErr: require.Error,
+ },
+ {
+ name: "invalid pragma format",
+ cfg: config{memory: false},
+ input: "PRAGMA invalid_format",
+ wantErr: require.Error,
+ },
+ {
+ name: "mmap_size pragma with memory DB",
+ cfg: config{memory: true},
+ input: "PRAGMA mmap_size=1000",
+ wantName: "mmap_size",
+ wantValue: "", // should be empty for memory DB
+ },
+ {
+ name: "mmap_size pragma with regular DB",
+ cfg: config{memory: false},
+ input: "PRAGMA mmap_size=1000",
+ wantName: "mmap_size",
+ wantValue: "1000",
+ },
+ {
+ name: "pragma with numeric value",
+ cfg: config{memory: false},
+ input: "PRAGMA page_size=4096",
+ wantName: "page_size",
+ wantValue: "4096",
+ },
+ {
+ name: "pragma with mixed case",
+ cfg: config{memory: false},
+ input: "PRAGMA Journal_Mode=WAL",
+ wantName: "journal_mode",
+ wantValue: "WAL",
+ },
+ {
+ name: "empty pragma",
+ cfg: config{memory: false},
+ input: "PRAGMA =value",
+ wantErr: require.Error,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.wantErr == nil {
+ tt.wantErr = require.NoError
+ }
+
+ gotName, gotValue, err := tt.cfg.pragmaNameValue(tt.input)
+ tt.wantErr(t, err)
+
+ if err == nil {
+ require.Equal(t, tt.wantName, gotName)
+ require.Equal(t, tt.wantValue, gotValue)
+ }
+ })
+ }
+}
diff --git a/grype/db/v1/id.go b/grype/db/v1/id.go
deleted file mode 100644
index 297d73c1303..00000000000
--- a/grype/db/v1/id.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package v1
-
-import (
- "time"
-)
-
-// ID represents identifying information for a DB and the data it contains.
-type ID struct {
- // BuildTimestamp is the timestamp used to define the age of the DB, ideally including the age of the data
- // contained in the DB, not just when the DB file was created.
- BuildTimestamp time.Time
- SchemaVersion int
-}
-
-type IDReader interface {
- GetID() (*ID, error)
-}
-
-type IDWriter interface {
- SetID(ID) error
-}
-
-func NewID(age time.Time) ID {
- return ID{
- BuildTimestamp: age.UTC(),
- SchemaVersion: SchemaVersion,
- }
-}
diff --git a/grype/db/v1/namespace.go b/grype/db/v1/namespace.go
deleted file mode 100644
index 780cbcfdf50..00000000000
--- a/grype/db/v1/namespace.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package v1
-
-import (
- "fmt"
-)
-
-const (
- NVDNamespace = "nvd"
-)
-
-func RecordSource(feed, group string) string {
- switch feed {
- case "github", "nvdv2":
- return group
- default:
- return fmt.Sprintf("%s:%s", feed, group)
- }
-}
-
-func NamespaceForFeedGroup(feed, group string) (string, error) {
- switch {
- case feed == "vulnerabilities":
- return group, nil
- case feed == "github":
- return group, nil
- case feed == "nvdv2" && group == "nvdv2:cves":
- return NVDNamespace, nil
- }
- return "", fmt.Errorf("feed=%q group=%q has no namespace mappings", feed, group)
-}
diff --git a/grype/db/v1/namespace_test.go b/grype/db/v1/namespace_test.go
deleted file mode 100644
index 10dc9845437..00000000000
--- a/grype/db/v1/namespace_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package v1
-
-import (
- "fmt"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestNamespaceFromRecordSource(t *testing.T) {
- tests := []struct {
- Feed, Group string
- Namespace string
- }{
- {
- Feed: "vulnerabilities",
- Group: "ubuntu:20.04",
- Namespace: "ubuntu:20.04",
- },
- {
- Feed: "vulnerabilities",
- Group: "alpine:3.9",
- Namespace: "alpine:3.9",
- },
- {
- Feed: "vulnerabilities",
- Group: "sles:12.5",
- Namespace: "sles:12.5",
- },
- {
- Feed: "nvdv2",
- Group: "nvdv2:cves",
- Namespace: "nvd",
- },
- {
- Feed: "github",
- Group: "github:python",
- Namespace: "github:python",
- },
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("feed=%q group=%q namespace=%q", test.Feed, test.Group, test.Namespace), func(t *testing.T) {
- actual, err := NamespaceForFeedGroup(test.Feed, test.Group)
- assert.NoError(t, err)
- assert.Equal(t, test.Namespace, actual)
- })
- }
-}
diff --git a/grype/db/v1/schema_version.go b/grype/db/v1/schema_version.go
deleted file mode 100644
index f72f10ceae8..00000000000
--- a/grype/db/v1/schema_version.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package v1
-
-const SchemaVersion = 1
diff --git a/grype/db/v1/store.go b/grype/db/v1/store.go
deleted file mode 100644
index 97b6f8bf683..00000000000
--- a/grype/db/v1/store.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package v1
-
-type Store interface {
- StoreReader
- StoreWriter
-}
-
-type StoreReader interface {
- IDReader
- VulnerabilityStoreReader
- VulnerabilityMetadataStoreReader
-}
-
-type StoreWriter interface {
- IDWriter
- VulnerabilityStoreWriter
- VulnerabilityMetadataStoreWriter
- Close()
-}
diff --git a/grype/db/v1/store/model/id.go b/grype/db/v1/store/model/id.go
deleted file mode 100644
index 51befc235c9..00000000000
--- a/grype/db/v1/store/model/id.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package model
-
-import (
- "fmt"
- "time"
-
- v1 "github.com/anchore/grype/grype/db/v1"
-)
-
-const (
- IDTableName = "id"
-)
-
-type IDModel struct {
- BuildTimestamp string `gorm:"column:build_timestamp"`
- SchemaVersion int `gorm:"column:schema_version"`
-}
-
-func NewIDModel(id v1.ID) IDModel {
- return IDModel{
- BuildTimestamp: id.BuildTimestamp.Format(time.RFC3339Nano),
- SchemaVersion: id.SchemaVersion,
- }
-}
-
-func (IDModel) TableName() string {
- return IDTableName
-}
-
-func (m *IDModel) Inflate() (v1.ID, error) {
- buildTime, err := time.Parse(time.RFC3339Nano, m.BuildTimestamp)
- if err != nil {
- return v1.ID{}, fmt.Errorf("unable to parse build timestamp (%+v): %w", m.BuildTimestamp, err)
- }
-
- return v1.ID{
- BuildTimestamp: buildTime,
- SchemaVersion: m.SchemaVersion,
- }, nil
-}
diff --git a/grype/db/v1/store/model/vulnerability.go b/grype/db/v1/store/model/vulnerability.go
deleted file mode 100644
index c52e4c274d3..00000000000
--- a/grype/db/v1/store/model/vulnerability.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package model
-
-import (
- "encoding/json"
- "fmt"
-
- v1 "github.com/anchore/grype/grype/db/v1"
-)
-
-const (
- VulnerabilityTableName = "vulnerability"
- GetVulnerabilityIndexName = "get_vulnerability_index"
-)
-
-// VulnerabilityModel is a struct used to serialize db.Vulnerability information into a sqlite3 DB.
-type VulnerabilityModel struct {
- PK uint64 `gorm:"primary_key;auto_increment;"`
- ID string `gorm:"column:id"`
- RecordSource string `gorm:"column:record_source"`
- PackageName string `gorm:"column:package_name; index:get_vulnerability_index"`
- Namespace string `gorm:"column:namespace; index:get_vulnerability_index"`
- VersionConstraint string `gorm:"column:version_constraint"`
- VersionFormat string `gorm:"column:version_format"`
- CPEs string `gorm:"column:cpes"`
- ProxyVulnerabilities string `gorm:"column:proxy_vulnerabilities"`
- FixedInVersion string `gorm:"column:fixed_in_version"`
-}
-
-// NewVulnerabilityModel generates a new model from a db.Vulnerability struct.
-func NewVulnerabilityModel(vulnerability v1.Vulnerability) VulnerabilityModel {
- cpes, err := json.Marshal(vulnerability.CPEs)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- proxy, err := json.Marshal(vulnerability.ProxyVulnerabilities)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- return VulnerabilityModel{
- ID: vulnerability.ID,
- PackageName: vulnerability.PackageName,
- RecordSource: vulnerability.RecordSource,
- Namespace: vulnerability.Namespace,
- VersionConstraint: vulnerability.VersionConstraint,
- VersionFormat: vulnerability.VersionFormat,
- FixedInVersion: vulnerability.FixedInVersion,
- CPEs: string(cpes),
- ProxyVulnerabilities: string(proxy),
- }
-}
-
-// TableName returns the table which all db.Vulnerability model instances are stored into.
-func (VulnerabilityModel) TableName() string {
- return VulnerabilityTableName
-}
-
-// Inflate generates a db.Vulnerability object from the serialized model instance.
-func (m *VulnerabilityModel) Inflate() (v1.Vulnerability, error) {
- var cpes []string
- err := json.Unmarshal([]byte(m.CPEs), &cpes)
- if err != nil {
- return v1.Vulnerability{}, fmt.Errorf("unable to unmarshal CPEs (%+v): %w", m.CPEs, err)
- }
-
- var proxy []string
- err = json.Unmarshal([]byte(m.ProxyVulnerabilities), &proxy)
- if err != nil {
- return v1.Vulnerability{}, fmt.Errorf("unable to unmarshal proxy vulnerabilities (%+v): %w", m.ProxyVulnerabilities, err)
- }
-
- return v1.Vulnerability{
- ID: m.ID,
- RecordSource: m.RecordSource,
- PackageName: m.PackageName,
- Namespace: m.Namespace,
- VersionConstraint: m.VersionConstraint,
- VersionFormat: m.VersionFormat,
- CPEs: cpes,
- ProxyVulnerabilities: proxy,
- FixedInVersion: m.FixedInVersion,
- }, nil
-}
diff --git a/grype/db/v1/store/model/vulnerability_metadata.go b/grype/db/v1/store/model/vulnerability_metadata.go
deleted file mode 100644
index 4c2bbba0bb9..00000000000
--- a/grype/db/v1/store/model/vulnerability_metadata.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package model
-
-import (
- "database/sql"
- "encoding/json"
- "fmt"
-
- v1 "github.com/anchore/grype/grype/db/v1"
-)
-
-const (
- VulnerabilityMetadataTableName = "vulnerability_metadata"
-)
-
-// VulnerabilityMetadataModel is a struct used to serialize db.VulnerabilityMetadata information into a sqlite3 DB.
-type VulnerabilityMetadataModel struct {
- ID string `gorm:"primary_key; column:id;"`
- RecordSource string `gorm:"primary_key; column:record_source;"`
- Severity string `gorm:"column:severity"`
- Links string `gorm:"column:links"`
- Description string `gorm:"column:description"`
- CvssV2 sql.NullString `gorm:"column:cvss_v2"`
- CvssV3 sql.NullString `gorm:"column:cvss_v3"`
-}
-
-// NewVulnerabilityMetadataModel generates a new model from a db.VulnerabilityMetadata struct.
-func NewVulnerabilityMetadataModel(metadata v1.VulnerabilityMetadata) VulnerabilityMetadataModel {
- links, err := json.Marshal(metadata.Links)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- var cvssV2Str sql.NullString
- if metadata.CvssV2 != nil {
- cvssV2, err := json.Marshal(*metadata.CvssV2)
- if err != nil {
- // TODO: just no
- panic(err)
- }
- cvssV2Str.String = string(cvssV2)
- cvssV2Str.Valid = true
- }
-
- var cvssV3Str sql.NullString
- if metadata.CvssV3 != nil {
- cvssV3, err := json.Marshal(*metadata.CvssV3)
- if err != nil {
- // TODO: just no
- panic(err)
- }
- cvssV3Str.String = string(cvssV3)
- cvssV3Str.Valid = true
- }
-
- return VulnerabilityMetadataModel{
- ID: metadata.ID,
- RecordSource: metadata.RecordSource,
- Severity: metadata.Severity,
- Links: string(links),
- Description: metadata.Description,
- CvssV2: cvssV2Str,
- CvssV3: cvssV3Str,
- }
-}
-
-// TableName returns the table which all db.VulnerabilityMetadata model instances are stored into.
-func (VulnerabilityMetadataModel) TableName() string {
- return VulnerabilityMetadataTableName
-}
-
-// Inflate generates a db.VulnerabilityMetadataModel object from the serialized model instance.
-func (m *VulnerabilityMetadataModel) Inflate() (v1.VulnerabilityMetadata, error) {
- var links []string
- var cvssV2, cvssV3 *v1.Cvss
-
- if err := json.Unmarshal([]byte(m.Links), &links); err != nil {
- return v1.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal links (%+v): %w", m.Links, err)
- }
-
- if m.CvssV2.Valid {
- err := json.Unmarshal([]byte(m.CvssV2.String), &cvssV2)
- if err != nil {
- return v1.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvssV2 data (%+v): %w", m.CvssV2, err)
- }
- }
-
- if m.CvssV3.Valid {
- err := json.Unmarshal([]byte(m.CvssV3.String), &cvssV3)
- if err != nil {
- return v1.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvssV3 data (%+v): %w", m.CvssV3, err)
- }
- }
-
- return v1.VulnerabilityMetadata{
- ID: m.ID,
- RecordSource: m.RecordSource,
- Severity: m.Severity,
- Links: links,
- Description: m.Description,
- CvssV2: cvssV2,
- CvssV3: cvssV3,
- }, nil
-}
diff --git a/grype/db/v1/store/store.go b/grype/db/v1/store/store.go
deleted file mode 100644
index 02656bd9317..00000000000
--- a/grype/db/v1/store/store.go
+++ /dev/null
@@ -1,211 +0,0 @@
-package store
-
-import (
- "fmt"
- "sort"
-
- "github.com/go-test/deep"
- "gorm.io/gorm"
-
- "github.com/anchore/grype/grype/db/internal/gormadapter"
- v1 "github.com/anchore/grype/grype/db/v1"
- "github.com/anchore/grype/grype/db/v1/store/model"
- "github.com/anchore/grype/internal/stringutil"
- _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
-)
-
-// store holds an instance of the database connection
-type store struct {
- db *gorm.DB
-}
-
-// New creates a new instance of the store.
-func New(dbFilePath string, overwrite bool) (v1.Store, error) {
- db, err := gormadapter.Open(dbFilePath, overwrite)
- if err != nil {
- return nil, err
- }
-
- if overwrite {
- // TODO: automigrate could write to the database,
- // we should be validating the database is the correct database based on the version in the ID table before
- // automigrating
- if err := db.AutoMigrate(&model.IDModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate ID model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityMetadataModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability Metadata model: %w", err)
- }
- }
-
- return &store{
- db: db,
- }, nil
-}
-
-// GetID fetches the metadata about the databases schema version and build time.
-func (s *store) GetID() (*v1.ID, error) {
- var models []model.IDModel
- result := s.db.Find(&models)
- if result.Error != nil {
- return nil, result.Error
- }
-
- switch {
- case len(models) > 1:
- return nil, fmt.Errorf("found multiple DB IDs")
- case len(models) == 1:
- id, err := models[0].Inflate()
- if err != nil {
- return nil, err
- }
- return &id, nil
- }
-
- return nil, nil
-}
-
-// SetID stores the databases schema version and build time.
-func (s *store) SetID(id v1.ID) error {
- var ids []model.IDModel
-
- // replace the existing ID with the given one
- s.db.Find(&ids).Delete(&ids)
-
- m := model.NewIDModel(id)
- result := s.db.Create(&m)
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add id (%d rows affected)", result.RowsAffected)
- }
-
- return result.Error
-}
-
-// GetVulnerability retrieves one or more vulnerabilities given a namespace and package name.
-func (s *store) GetVulnerability(namespace, packageName string) ([]v1.Vulnerability, error) {
- var models []model.VulnerabilityModel
-
- result := s.db.Where("namespace = ? AND package_name = ?", namespace, packageName).Find(&models)
-
- var vulnerabilities = make([]v1.Vulnerability, len(models))
- for idx, m := range models {
- vulnerability, err := m.Inflate()
- if err != nil {
- return nil, err
- }
- vulnerabilities[idx] = vulnerability
- }
-
- return vulnerabilities, result.Error
-}
-
-// AddVulnerability saves one or more vulnerabilities into the sqlite3 store.
-func (s *store) AddVulnerability(vulnerabilities ...v1.Vulnerability) error {
- for _, vulnerability := range vulnerabilities {
- m := model.NewVulnerabilityModel(vulnerability)
-
- result := s.db.Create(&m)
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add vulnerability (%d rows affected)", result.RowsAffected)
- }
- }
- return nil
-}
-
-// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
-func (s *store) GetVulnerabilityMetadata(id, recordSource string) (*v1.VulnerabilityMetadata, error) {
- var models []model.VulnerabilityMetadataModel
-
- result := s.db.Where(&model.VulnerabilityMetadataModel{ID: id, RecordSource: recordSource}).Find(&models)
- if result.Error != nil {
- return nil, result.Error
- }
-
- switch {
- case len(models) > 1:
- return nil, fmt.Errorf("found multiple metadatas for single ID=%q RecordSource=%q", id, recordSource)
- case len(models) == 1:
- metadata, err := models[0].Inflate()
- if err != nil {
- return nil, err
- }
-
- return &metadata, nil
- }
-
- return nil, nil
-}
-
-// AddVulnerabilityMetadata stores one or more vulnerability metadata models into the sqlite DB.
-func (s *store) AddVulnerabilityMetadata(metadata ...v1.VulnerabilityMetadata) error {
- for _, m := range metadata {
- existing, err := s.GetVulnerabilityMetadata(m.ID, m.RecordSource)
- if err != nil {
- return fmt.Errorf("failed to verify existing entry: %w", err)
- }
-
- if existing != nil {
- // merge with the existing entry
-
- cvssV3Diffs := deep.Equal(existing.CvssV3, m.CvssV3)
- cvssV2Diffs := deep.Equal(existing.CvssV2, m.CvssV2)
-
- switch {
- case existing.Severity != m.Severity:
- return fmt.Errorf("existing metadata has mismatched severity (%q!=%q)", existing.Severity, m.Severity)
- case existing.Description != m.Description:
- return fmt.Errorf("existing metadata has mismatched description (%q!=%q)", existing.Description, m.Description)
- case existing.CvssV2 != nil && len(cvssV2Diffs) > 0:
- return fmt.Errorf("existing metadata has mismatched cvss-v2: %+v", cvssV2Diffs)
- case existing.CvssV3 != nil && len(cvssV3Diffs) > 0:
- return fmt.Errorf("existing metadata has mismatched cvss-v3: %+v", cvssV3Diffs)
- default:
- existing.CvssV2 = m.CvssV2
- existing.CvssV3 = m.CvssV3
- }
-
- links := stringutil.NewStringSetFromSlice(existing.Links)
- for _, l := range m.Links {
- links.Add(l)
- }
-
- existing.Links = links.ToSlice()
- sort.Strings(existing.Links)
-
- newModel := model.NewVulnerabilityMetadataModel(*existing)
- result := s.db.Save(&newModel)
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to merge vulnerability metadata (%d rows affected)", result.RowsAffected)
- }
-
- if result.Error != nil {
- return result.Error
- }
- } else {
- // this is a new entry
- newModel := model.NewVulnerabilityMetadataModel(m)
- result := s.db.Create(&newModel)
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add vulnerability metadata (%d rows affected)", result.RowsAffected)
- }
- }
- }
- return nil
-}
-
-func (s *store) Close() {
- s.db.Exec("VACUUM;")
-}
diff --git a/grype/db/v1/store/store_test.go b/grype/db/v1/store/store_test.go
deleted file mode 100644
index 99019a7a0d8..00000000000
--- a/grype/db/v1/store/store_test.go
+++ /dev/null
@@ -1,494 +0,0 @@
-package store
-
-import (
- "testing"
- "time"
-
- "github.com/go-test/deep"
-
- v1 "github.com/anchore/grype/grype/db/v1"
- "github.com/anchore/grype/grype/db/v1/store/model"
-)
-
-func assertIDReader(t *testing.T, reader v1.IDReader, expected v1.ID) {
- t.Helper()
- if actual, err := reader.GetID(); err != nil {
- t.Fatalf("failed to get ID: %+v", err)
- } else {
- diffs := deep.Equal(&expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
-}
-
-func TestStore_GetID_SetID(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- expected := v1.ID{
- BuildTimestamp: time.Now().UTC(),
- SchemaVersion: 2,
- }
-
- if err = s.SetID(expected); err != nil {
- t.Fatalf("failed to set ID: %+v", err)
- }
-
- assertIDReader(t, s, expected)
-
-}
-
-func assertVulnerabilityReader(t *testing.T, reader v1.VulnerabilityStoreReader, namespace, name string, expected []v1.Vulnerability) {
- if actual, err := reader.GetVulnerability(namespace, name); err != nil {
- t.Fatalf("failed to get Vulnerability: %+v", err)
- } else {
- if len(actual) != len(expected) {
- t.Fatalf("unexpected number of vulns: %d", len(actual))
- }
-
- for idx := range actual {
- diffs := deep.Equal(expected[idx], actual[idx])
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
- }
-}
-
-func TestStore_GetVulnerability_SetVulnerability(t *testing.T) {
- dbTempFile := t.TempDir()
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- extra := []v1.Vulnerability{
- {
- ID: "my-cve-33333",
- RecordSource: "record-source",
- PackageName: "package-name-2",
- Namespace: "my-namespace",
- VersionConstraint: "< 1.0",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
- FixedInVersion: "2.0.1",
- },
- {
- ID: "my-other-cve-33333",
- RecordSource: "record-source",
- PackageName: "package-name-3",
- Namespace: "my-namespace",
- VersionConstraint: "< 509.2.2",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
- },
- }
-
- expected := []v1.Vulnerability{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 1.0",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
- FixedInVersion: "1.0.1",
- },
- {
- ID: "my-other-cve",
- RecordSource: "record-source",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 509.2.2",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
- FixedInVersion: "4.0.5",
- },
- }
-
- total := append(expected, extra...)
-
- if err = s.AddVulnerability(total...); err != nil {
- t.Fatalf("failed to set Vulnerability: %+v", err)
- }
-
- var allEntries []model.VulnerabilityModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != len(total) {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- assertVulnerabilityReader(t, s, expected[0].Namespace, expected[0].PackageName, expected)
-
-}
-
-func assertVulnerabilityMetadataReader(t *testing.T, reader v1.VulnerabilityMetadataStoreReader, id, recordSource string, expected v1.VulnerabilityMetadata) {
- if actual, err := reader.GetVulnerabilityMetadata(id, recordSource); err != nil {
- t.Fatalf("failed to get metadata: %+v", err)
- } else {
-
- diffs := deep.Equal(&expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
-
-}
-
-func TestStore_GetVulnerabilityMetadata_SetVulnerabilityMetadata(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- total := []v1.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 1.1,
- ExploitabilityScore: 2.2,
- ImpactScore: 3.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NOT",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.3,
- ExploitabilityScore: 2.1,
- ImpactScore: 3.2,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NICE",
- },
- },
- {
- ID: "my-other-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "worst description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- }
-
- if err = s.AddVulnerabilityMetadata(total...); err != nil {
- t.Fatalf("failed to set metadata: %+v", err)
- }
-
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != len(total) {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
-}
-
-func TestStore_MergeVulnerabilityMetadata(t *testing.T) {
- tests := []struct {
- name string
- add []v1.VulnerabilityMetadata
- expected v1.VulnerabilityMetadata
- err bool
- }{
- {
- name: "go-case",
- add: []v1.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "worst description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- expected: v1.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "worst description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- name: "merge-links",
- add: []v1.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://google.com"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://yahoo.com"},
- },
- },
- expected: v1.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re", "https://google.com", "https://yahoo.com"},
- },
- },
- {
- name: "bad-severity",
- add: []v1.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "meh, push that for next tuesday...",
- Links: []string{"https://redhat.com"},
- },
- },
- err: true,
- },
- {
- name: "mismatch-description",
- err: true,
- add: []v1.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "worst description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- {
- name: "mismatch-cvss2",
- err: true,
- add: []v1.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:P--VERY",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- {
- name: "mismatch-cvss3",
- err: true,
- add: []v1.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v1.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v1.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 0,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- dbTempDir := t.TempDir()
-
- s, err := New(dbTempDir, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- // add each metadata in order
- var theErr error
- for _, metadata := range test.add {
- err = s.AddVulnerabilityMetadata(metadata)
- if err != nil {
- theErr = err
- break
- }
- }
-
- if test.err && theErr == nil {
- t.Fatalf("expected error but did not get one")
- } else if !test.err && theErr != nil {
- t.Fatalf("expected no error but got one: %+v", theErr)
- } else if test.err && theErr != nil {
- // test pass...
- return
- }
-
- // ensure there is exactly one entry
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != 1 {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- // get the resulting metadata object
- if actual, err := s.GetVulnerabilityMetadata(test.expected.ID, test.expected.RecordSource); err != nil {
- t.Fatalf("failed to get metadata: %+v", err)
- } else {
- diffs := deep.Equal(&test.expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
- })
- }
-}
diff --git a/grype/db/v1/vulnerability.go b/grype/db/v1/vulnerability.go
deleted file mode 100644
index c0fa5912ea8..00000000000
--- a/grype/db/v1/vulnerability.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package v1
-
-const VulnerabilityStoreFileName = "vulnerability.db"
-
-// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.
-type Vulnerability struct {
- ID string // The identifier of the vulnerability or advisory
- RecordSource string // The source of the vulnerability information
- PackageName string // The name of the package that is vulnerable
- Namespace string // The ecosystem where the package resides
- VersionConstraint string // The version range which the given package is vulnerable
- VersionFormat string // The format which all version fields should be interpreted as
- CPEs []string // The CPEs which are considered vulnerable
- ProxyVulnerabilities []string // IDs of other Vulnerabilities that are related to this one (this is how advisories relate to CVEs)
- FixedInVersion string // The version which this particular vulnerability was fixed in
-}
-
-type VulnerabilityStore interface {
- VulnerabilityStoreReader
- VulnerabilityStoreWriter
-}
-
-type VulnerabilityStoreReader interface {
- // GetVulnerability retrieves vulnerabilities associated with a namespace and a package name
- GetVulnerability(namespace, name string) ([]Vulnerability, error)
-}
-
-type VulnerabilityStoreWriter interface {
- // AddVulnerability inserts a new record of a vulnerability into the store
- AddVulnerability(vulnerabilities ...Vulnerability) error
-}
diff --git a/grype/db/v1/vulnerability_metadata.go b/grype/db/v1/vulnerability_metadata.go
deleted file mode 100644
index 218c0ff5d9a..00000000000
--- a/grype/db/v1/vulnerability_metadata.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package v1
-
-// VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching.
-type VulnerabilityMetadata struct {
- ID string // The identifier of the vulnerability or advisory
- RecordSource string // The source of the vulnerability information
- Severity string // How severe the vulnerability is (valid values are defined by upstream sources currently)
- Links []string // URLs to get more information about the vulnerability or advisory
- Description string // Description of the vulnerability
- CvssV2 *Cvss // Common Vulnerability Scoring System V2 values
- CvssV3 *Cvss // Common Vulnerability Scoring System V3 values
-}
-
-// Cvss contains select Common Vulnerability Scoring System fields for a vulnerability.
-type Cvss struct {
- BaseScore float64 // Ranges from 0 - 10 and defines for qualities intrinsic to a vulnerability
- ExploitabilityScore float64 // Indicator of how easy it may be for an attacker to exploit a vulnerability
- ImpactScore float64 // Representation of the effects of an exploited vulnerability relative to compromise in confidentiality, integrity, and availability
- Vector string // A textual representation of the metric values used to determine the score
-}
-
-type VulnerabilityMetadataStore interface {
- VulnerabilityMetadataStoreReader
- VulnerabilityMetadataStoreWriter
-}
-
-type VulnerabilityMetadataStoreReader interface {
- GetVulnerabilityMetadata(id, recordSource string) (*VulnerabilityMetadata, error)
-}
-
-type VulnerabilityMetadataStoreWriter interface {
- AddVulnerabilityMetadata(metadata ...VulnerabilityMetadata) error
-}
diff --git a/grype/db/v2/id.go b/grype/db/v2/id.go
deleted file mode 100644
index f162ea08d48..00000000000
--- a/grype/db/v2/id.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package v2
-
-import (
- "time"
-)
-
-// ID represents identifying information for a DB and the data it contains.
-type ID struct {
- // BuildTimestamp is the timestamp used to define the age of the DB, ideally including the age of the data
- // contained in the DB, not just when the DB file was created.
- BuildTimestamp time.Time
- SchemaVersion int
-}
-
-type IDReader interface {
- GetID() (*ID, error)
-}
-
-type IDWriter interface {
- SetID(ID) error
-}
-
-func NewID(age time.Time) ID {
- return ID{
- BuildTimestamp: age.UTC(),
- SchemaVersion: SchemaVersion,
- }
-}
diff --git a/grype/db/v2/namespace.go b/grype/db/v2/namespace.go
deleted file mode 100644
index 70ac1030d4c..00000000000
--- a/grype/db/v2/namespace.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package v2
-
-import (
- "fmt"
-)
-
-const (
- NVDNamespace = "nvd"
-)
-
-func RecordSource(feed, group string) string {
- switch feed {
- case "github", "nvdv2":
- return group
- default:
- return fmt.Sprintf("%s:%s", feed, group)
- }
-}
-
-func NamespaceForFeedGroup(feed, group string) (string, error) {
- switch {
- case feed == "vulnerabilities":
- return group, nil
- case feed == "github":
- return group, nil
- case feed == "nvdv2" && group == "nvdv2:cves":
- return NVDNamespace, nil
- }
- return "", fmt.Errorf("feed=%q group=%q has no namespace mappings", feed, group)
-}
diff --git a/grype/db/v2/namespace_test.go b/grype/db/v2/namespace_test.go
deleted file mode 100644
index f4f4a2bb223..00000000000
--- a/grype/db/v2/namespace_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package v2
-
-import (
- "fmt"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestNamespaceFromRecordSource(t *testing.T) {
- tests := []struct {
- Feed, Group string
- Namespace string
- }{
- {
- Feed: "vulnerabilities",
- Group: "ubuntu:20.04",
- Namespace: "ubuntu:20.04",
- },
- {
- Feed: "vulnerabilities",
- Group: "alpine:3.9",
- Namespace: "alpine:3.9",
- },
- {
- Feed: "vulnerabilities",
- Group: "sles:12.5",
- Namespace: "sles:12.5",
- },
- {
- Feed: "nvdv2",
- Group: "nvdv2:cves",
- Namespace: "nvd",
- },
- {
- Feed: "github",
- Group: "github:python",
- Namespace: "github:python",
- },
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("feed=%q group=%q namespace=%q", test.Feed, test.Group, test.Namespace), func(t *testing.T) {
- actual, err := NamespaceForFeedGroup(test.Feed, test.Group)
- assert.NoError(t, err)
- assert.Equal(t, test.Namespace, actual)
- })
- }
-}
diff --git a/grype/db/v2/schema_version.go b/grype/db/v2/schema_version.go
deleted file mode 100644
index 86d7d191d8b..00000000000
--- a/grype/db/v2/schema_version.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package v2
-
-const SchemaVersion = 2
diff --git a/grype/db/v2/store.go b/grype/db/v2/store.go
deleted file mode 100644
index ad5cb06484b..00000000000
--- a/grype/db/v2/store.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package v2
-
-type Store interface {
- StoreReader
- StoreWriter
-}
-
-type StoreReader interface {
- IDReader
- VulnerabilityStoreReader
- VulnerabilityMetadataStoreReader
-}
-
-type StoreWriter interface {
- IDWriter
- VulnerabilityStoreWriter
- VulnerabilityMetadataStoreWriter
- Close()
-}
diff --git a/grype/db/v2/store/model/id.go b/grype/db/v2/store/model/id.go
deleted file mode 100644
index 03185af0ee4..00000000000
--- a/grype/db/v2/store/model/id.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package model
-
-import (
- "fmt"
- "time"
-
- v2 "github.com/anchore/grype/grype/db/v2"
-)
-
-const (
- IDTableName = "id"
-)
-
-type IDModel struct {
- BuildTimestamp string `gorm:"column:build_timestamp"`
- SchemaVersion int `gorm:"column:schema_version"`
-}
-
-func NewIDModel(id v2.ID) IDModel {
- return IDModel{
- BuildTimestamp: id.BuildTimestamp.Format(time.RFC3339Nano),
- SchemaVersion: id.SchemaVersion,
- }
-}
-
-func (IDModel) TableName() string {
- return IDTableName
-}
-
-func (m *IDModel) Inflate() (v2.ID, error) {
- buildTime, err := time.Parse(time.RFC3339Nano, m.BuildTimestamp)
- if err != nil {
- return v2.ID{}, fmt.Errorf("unable to parse build timestamp (%+v): %w", m.BuildTimestamp, err)
- }
-
- return v2.ID{
- BuildTimestamp: buildTime,
- SchemaVersion: m.SchemaVersion,
- }, nil
-}
diff --git a/grype/db/v2/store/model/vulnerability.go b/grype/db/v2/store/model/vulnerability.go
deleted file mode 100644
index 25f8c18d27f..00000000000
--- a/grype/db/v2/store/model/vulnerability.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package model
-
-import (
- "encoding/json"
- "fmt"
-
- v2 "github.com/anchore/grype/grype/db/v2"
-)
-
-const (
- VulnerabilityTableName = "vulnerability"
- GetVulnerabilityIndexName = "get_vulnerability_index"
-)
-
-// VulnerabilityModel is a struct used to serialize db.Vulnerability information into a sqlite3 DB.
-type VulnerabilityModel struct {
- PK uint64 `gorm:"primary_key;auto_increment;"`
- ID string `gorm:"column:id"`
- RecordSource string `gorm:"column:record_source"`
- PackageName string `gorm:"column:package_name; index:get_vulnerability_index"`
- Namespace string `gorm:"column:namespace; index:get_vulnerability_index"`
- VersionConstraint string `gorm:"column:version_constraint"`
- VersionFormat string `gorm:"column:version_format"`
- CPEs string `gorm:"column:cpes"`
- ProxyVulnerabilities string `gorm:"column:proxy_vulnerabilities"`
- FixedInVersion string `gorm:"column:fixed_in_version"`
-}
-
-// NewVulnerabilityModel generates a new model from a db.Vulnerability struct.
-func NewVulnerabilityModel(vulnerability v2.Vulnerability) VulnerabilityModel {
- cpes, err := json.Marshal(vulnerability.CPEs)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- proxy, err := json.Marshal(vulnerability.ProxyVulnerabilities)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- return VulnerabilityModel{
- ID: vulnerability.ID,
- PackageName: vulnerability.PackageName,
- RecordSource: vulnerability.RecordSource,
- Namespace: vulnerability.Namespace,
- VersionConstraint: vulnerability.VersionConstraint,
- VersionFormat: vulnerability.VersionFormat,
- FixedInVersion: vulnerability.FixedInVersion,
- CPEs: string(cpes),
- ProxyVulnerabilities: string(proxy),
- }
-}
-
-// TableName returns the table which all db.Vulnerability model instances are stored into.
-func (VulnerabilityModel) TableName() string {
- return VulnerabilityTableName
-}
-
-// Inflate generates a db.Vulnerability object from the serialized model instance.
-func (m *VulnerabilityModel) Inflate() (v2.Vulnerability, error) {
- var cpes []string
- err := json.Unmarshal([]byte(m.CPEs), &cpes)
- if err != nil {
- return v2.Vulnerability{}, fmt.Errorf("unable to unmarshal CPEs (%+v): %w", m.CPEs, err)
- }
-
- var proxy []string
- err = json.Unmarshal([]byte(m.ProxyVulnerabilities), &proxy)
- if err != nil {
- return v2.Vulnerability{}, fmt.Errorf("unable to unmarshal proxy vulnerabilities (%+v): %w", m.ProxyVulnerabilities, err)
- }
-
- return v2.Vulnerability{
- ID: m.ID,
- RecordSource: m.RecordSource,
- PackageName: m.PackageName,
- Namespace: m.Namespace,
- VersionConstraint: m.VersionConstraint,
- VersionFormat: m.VersionFormat,
- CPEs: cpes,
- ProxyVulnerabilities: proxy,
- FixedInVersion: m.FixedInVersion,
- }, nil
-}
diff --git a/grype/db/v2/store/model/vulnerability_metadata.go b/grype/db/v2/store/model/vulnerability_metadata.go
deleted file mode 100644
index 47c47d9660e..00000000000
--- a/grype/db/v2/store/model/vulnerability_metadata.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package model
-
-import (
- "database/sql"
- "encoding/json"
- "fmt"
-
- v2 "github.com/anchore/grype/grype/db/v2"
-)
-
-const (
- VulnerabilityMetadataTableName = "vulnerability_metadata"
-)
-
-// VulnerabilityMetadataModel is a struct used to serialize db.VulnerabilityMetadata information into a sqlite3 DB.
-type VulnerabilityMetadataModel struct {
- ID string `gorm:"primary_key; column:id;"`
- RecordSource string `gorm:"primary_key; column:record_source;"`
- Severity string `gorm:"column:severity"`
- Links string `gorm:"column:links"`
- Description string `gorm:"column:description"`
- CvssV2 sql.NullString `gorm:"column:cvss_v2"`
- CvssV3 sql.NullString `gorm:"column:cvss_v3"`
-}
-
-// NewVulnerabilityMetadataModel generates a new model from a db.VulnerabilityMetadata struct.
-func NewVulnerabilityMetadataModel(metadata v2.VulnerabilityMetadata) VulnerabilityMetadataModel {
- links, err := json.Marshal(metadata.Links)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- var cvssV2Str sql.NullString
- if metadata.CvssV2 != nil {
- cvssV2, err := json.Marshal(*metadata.CvssV2)
- if err != nil {
- // TODO: just no
- panic(err)
- }
- cvssV2Str.String = string(cvssV2)
- cvssV2Str.Valid = true
- }
-
- var cvssV3Str sql.NullString
- if metadata.CvssV3 != nil {
- cvssV3, err := json.Marshal(*metadata.CvssV3)
- if err != nil {
- // TODO: just no
- panic(err)
- }
- cvssV3Str.String = string(cvssV3)
- cvssV3Str.Valid = true
- }
-
- return VulnerabilityMetadataModel{
- ID: metadata.ID,
- RecordSource: metadata.RecordSource,
- Severity: metadata.Severity,
- Links: string(links),
- Description: metadata.Description,
- CvssV2: cvssV2Str,
- CvssV3: cvssV3Str,
- }
-}
-
-// TableName returns the table which all db.VulnerabilityMetadata model instances are stored into.
-func (VulnerabilityMetadataModel) TableName() string {
- return VulnerabilityMetadataTableName
-}
-
-// Inflate generates a db.VulnerabilityMetadataModel object from the serialized model instance.
-func (m *VulnerabilityMetadataModel) Inflate() (v2.VulnerabilityMetadata, error) {
- var links []string
- var cvssV2, cvssV3 *v2.Cvss
-
- if err := json.Unmarshal([]byte(m.Links), &links); err != nil {
- return v2.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal links (%+v): %w", m.Links, err)
- }
-
- if m.CvssV2.Valid {
- err := json.Unmarshal([]byte(m.CvssV2.String), &cvssV2)
- if err != nil {
- return v2.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvssV2 data (%+v): %w", m.CvssV2, err)
- }
- }
-
- if m.CvssV3.Valid {
- err := json.Unmarshal([]byte(m.CvssV3.String), &cvssV3)
- if err != nil {
- return v2.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvssV3 data (%+v): %w", m.CvssV3, err)
- }
- }
-
- return v2.VulnerabilityMetadata{
- ID: m.ID,
- RecordSource: m.RecordSource,
- Severity: m.Severity,
- Links: links,
- Description: m.Description,
- CvssV2: cvssV2,
- CvssV3: cvssV3,
- }, nil
-}
diff --git a/grype/db/v2/store/store.go b/grype/db/v2/store/store.go
deleted file mode 100644
index b0d7907f636..00000000000
--- a/grype/db/v2/store/store.go
+++ /dev/null
@@ -1,210 +0,0 @@
-package store
-
-import (
- "fmt"
- "sort"
-
- "github.com/go-test/deep"
- "gorm.io/gorm"
-
- "github.com/anchore/grype/grype/db/internal/gormadapter"
- v2 "github.com/anchore/grype/grype/db/v2"
- "github.com/anchore/grype/grype/db/v2/store/model"
- "github.com/anchore/grype/internal/stringutil"
- _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
-)
-
-// store holds an instance of the database connection
-type store struct {
- db *gorm.DB
-}
-
-// New creates a new instance of the store.
-func New(dbFilePath string, overwrite bool) (v2.Store, error) {
- db, err := gormadapter.Open(dbFilePath, overwrite)
- if err != nil {
- return nil, err
- }
- if overwrite {
- // TODO: automigrate could write to the database,
- // we should be validating the database is the correct database based on the version in the ID table before
- // automigrating
- if err := db.AutoMigrate(&model.IDModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate ID model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityMetadataModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability Metadata model: %w", err)
- }
- }
-
- return &store{
- db: db,
- }, nil
-}
-
-// GetID fetches the metadata about the databases schema version and build time.
-func (s *store) GetID() (*v2.ID, error) {
- var models []model.IDModel
- result := s.db.Find(&models)
- if result.Error != nil {
- return nil, result.Error
- }
-
- switch {
- case len(models) > 1:
- return nil, fmt.Errorf("found multiple DB IDs")
- case len(models) == 1:
- id, err := models[0].Inflate()
- if err != nil {
- return nil, err
- }
- return &id, nil
- }
-
- return nil, nil
-}
-
-// SetID stores the databases schema version and build time.
-func (s *store) SetID(id v2.ID) error {
- var ids []model.IDModel
-
- // replace the existing ID with the given one
- s.db.Find(&ids).Delete(&ids)
-
- m := model.NewIDModel(id)
- result := s.db.Create(&m)
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add id (%d rows affected)", result.RowsAffected)
- }
-
- return result.Error
-}
-
-// GetVulnerability retrieves one or more vulnerabilities given a namespace and package name.
-func (s *store) GetVulnerability(namespace, packageName string) ([]v2.Vulnerability, error) {
- var models []model.VulnerabilityModel
-
- result := s.db.Where("namespace = ? AND package_name = ?", namespace, packageName).Find(&models)
-
- var vulnerabilities = make([]v2.Vulnerability, len(models))
- for idx, m := range models {
- vulnerability, err := m.Inflate()
- if err != nil {
- return nil, err
- }
- vulnerabilities[idx] = vulnerability
- }
-
- return vulnerabilities, result.Error
-}
-
-// AddVulnerability saves one or more vulnerabilities into the sqlite3 store.
-func (s *store) AddVulnerability(vulnerabilities ...v2.Vulnerability) error {
- for _, vulnerability := range vulnerabilities {
- m := model.NewVulnerabilityModel(vulnerability)
-
- result := s.db.Create(&m)
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add vulnerability (%d rows affected)", result.RowsAffected)
- }
- }
- return nil
-}
-
-// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
-func (s *store) GetVulnerabilityMetadata(id, recordSource string) (*v2.VulnerabilityMetadata, error) {
- var models []model.VulnerabilityMetadataModel
-
- result := s.db.Where(&model.VulnerabilityMetadataModel{ID: id, RecordSource: recordSource}).Find(&models)
- if result.Error != nil {
- return nil, result.Error
- }
-
- switch {
- case len(models) > 1:
- return nil, fmt.Errorf("found multiple metadatas for single ID=%q RecordSource=%q", id, recordSource)
- case len(models) == 1:
- metadata, err := models[0].Inflate()
- if err != nil {
- return nil, err
- }
-
- return &metadata, nil
- }
-
- return nil, nil
-}
-
-// AddVulnerabilityMetadata stores one or more vulnerability metadata models into the sqlite DB.
-func (s *store) AddVulnerabilityMetadata(metadata ...v2.VulnerabilityMetadata) error {
- for _, m := range metadata {
- existing, err := s.GetVulnerabilityMetadata(m.ID, m.RecordSource)
- if err != nil {
- return fmt.Errorf("failed to verify existing entry: %w", err)
- }
-
- if existing != nil {
- // merge with the existing entry
-
- cvssV3Diffs := deep.Equal(existing.CvssV3, m.CvssV3)
- cvssV2Diffs := deep.Equal(existing.CvssV2, m.CvssV2)
-
- switch {
- case existing.Severity != m.Severity:
- return fmt.Errorf("existing metadata has mismatched severity (%q!=%q)", existing.Severity, m.Severity)
- case existing.Description != m.Description:
- return fmt.Errorf("existing metadata has mismatched description (%q!=%q)", existing.Description, m.Description)
- case existing.CvssV2 != nil && len(cvssV2Diffs) > 0:
- return fmt.Errorf("existing metadata has mismatched cvss-v2: %+v", cvssV2Diffs)
- case existing.CvssV3 != nil && len(cvssV3Diffs) > 0:
- return fmt.Errorf("existing metadata has mismatched cvss-v3: %+v", cvssV3Diffs)
- default:
- existing.CvssV2 = m.CvssV2
- existing.CvssV3 = m.CvssV3
- }
-
- links := stringutil.NewStringSetFromSlice(existing.Links)
- for _, l := range m.Links {
- links.Add(l)
- }
-
- existing.Links = links.ToSlice()
- sort.Strings(existing.Links)
-
- newModel := model.NewVulnerabilityMetadataModel(*existing)
- result := s.db.Save(&newModel)
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to merge vulnerability metadata (%d rows affected)", result.RowsAffected)
- }
-
- if result.Error != nil {
- return result.Error
- }
- } else {
- // this is a new entry
- newModel := model.NewVulnerabilityMetadataModel(m)
- result := s.db.Create(&newModel)
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add vulnerability metadata (%d rows affected)", result.RowsAffected)
- }
- }
- }
- return nil
-}
-
-func (s *store) Close() {
- s.db.Exec("VACUUM;")
-}
diff --git a/grype/db/v2/store/store_test.go b/grype/db/v2/store/store_test.go
deleted file mode 100644
index 43537be52a7..00000000000
--- a/grype/db/v2/store/store_test.go
+++ /dev/null
@@ -1,494 +0,0 @@
-package store
-
-import (
- "testing"
- "time"
-
- "github.com/go-test/deep"
-
- v2 "github.com/anchore/grype/grype/db/v2"
- "github.com/anchore/grype/grype/db/v2/store/model"
-)
-
-func assertIDReader(t *testing.T, reader v2.IDReader, expected v2.ID) {
- t.Helper()
- if actual, err := reader.GetID(); err != nil {
- t.Fatalf("failed to get ID: %+v", err)
- } else {
- diffs := deep.Equal(&expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
-}
-
-func TestStore_GetID_SetID(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- expected := v2.ID{
- BuildTimestamp: time.Now().UTC(),
- SchemaVersion: 2,
- }
-
- if err = s.SetID(expected); err != nil {
- t.Fatalf("failed to set ID: %+v", err)
- }
-
- assertIDReader(t, s, expected)
-
-}
-
-func assertVulnerabilityReader(t *testing.T, reader v2.VulnerabilityStoreReader, namespace, name string, expected []v2.Vulnerability) {
- if actual, err := reader.GetVulnerability(namespace, name); err != nil {
- t.Fatalf("failed to get Vulnerability: %+v", err)
- } else {
- if len(actual) != len(expected) {
- t.Fatalf("unexpected number of vulns: %d", len(actual))
- }
-
- for idx := range actual {
- diffs := deep.Equal(expected[idx], actual[idx])
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
- }
-}
-
-func TestStore_GetVulnerability_SetVulnerability(t *testing.T) {
- dbTempFile := t.TempDir()
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- extra := []v2.Vulnerability{
- {
- ID: "my-cve-33333",
- RecordSource: "record-source",
- PackageName: "package-name-2",
- Namespace: "my-namespace",
- VersionConstraint: "< 1.0",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
- FixedInVersion: "2.0.1",
- },
- {
- ID: "my-other-cve-33333",
- RecordSource: "record-source",
- PackageName: "package-name-3",
- Namespace: "my-namespace",
- VersionConstraint: "< 509.2.2",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
- },
- }
-
- expected := []v2.Vulnerability{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 1.0",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
- FixedInVersion: "1.0.1",
- },
- {
- ID: "my-other-cve",
- RecordSource: "record-source",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 509.2.2",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
- FixedInVersion: "4.0.5",
- },
- }
-
- total := append(expected, extra...)
-
- if err = s.AddVulnerability(total...); err != nil {
- t.Fatalf("failed to set Vulnerability: %+v", err)
- }
-
- var allEntries []model.VulnerabilityModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != len(total) {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- assertVulnerabilityReader(t, s, expected[0].Namespace, expected[0].PackageName, expected)
-
-}
-
-func assertVulnerabilityMetadataReader(t *testing.T, reader v2.VulnerabilityMetadataStoreReader, id, recordSource string, expected v2.VulnerabilityMetadata) {
- if actual, err := reader.GetVulnerabilityMetadata(id, recordSource); err != nil {
- t.Fatalf("failed to get metadata: %+v", err)
- } else {
-
- diffs := deep.Equal(&expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
-
-}
-
-func TestStore_GetVulnerabilityMetadata_SetVulnerabilityMetadata(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- total := []v2.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 1.1,
- ExploitabilityScore: 2.2,
- ImpactScore: 3.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NOT",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.3,
- ExploitabilityScore: 2.1,
- ImpactScore: 3.2,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NICE",
- },
- },
- {
- ID: "my-other-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "worst description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- }
-
- if err = s.AddVulnerabilityMetadata(total...); err != nil {
- t.Fatalf("failed to set metadata: %+v", err)
- }
-
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != len(total) {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
-}
-
-func TestStore_MergeVulnerabilityMetadata(t *testing.T) {
- tests := []struct {
- name string
- add []v2.VulnerabilityMetadata
- expected v2.VulnerabilityMetadata
- err bool
- }{
- {
- name: "go-case",
- add: []v2.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "worst description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- expected: v2.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "worst description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- name: "merge-links",
- add: []v2.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://google.com"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://yahoo.com"},
- },
- },
- expected: v2.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re", "https://google.com", "https://yahoo.com"},
- },
- },
- {
- name: "bad-severity",
- add: []v2.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "meh, push that for next tuesday...",
- Links: []string{"https://redhat.com"},
- },
- },
- err: true,
- },
- {
- name: "mismatch-description",
- err: true,
- add: []v2.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "worst description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- {
- name: "mismatch-cvss2",
- err: true,
- add: []v2.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:P--VERY",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- {
- name: "mismatch-cvss3",
- err: true,
- add: []v2.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 2.5,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Severity: "pretty bad",
- Links: []string{"https://ancho.re"},
- Description: "best description ever",
- CvssV2: &v2.Cvss{
- BaseScore: 4.1,
- ExploitabilityScore: 5.2,
- ImpactScore: 6.3,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- CvssV3: &v2.Cvss{
- BaseScore: 1.4,
- ExploitabilityScore: 0,
- ImpactScore: 3.6,
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- dbTempDir := t.TempDir()
-
- s, err := New(dbTempDir, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- // add each metadata in order
- var theErr error
- for _, metadata := range test.add {
- err = s.AddVulnerabilityMetadata(metadata)
- if err != nil {
- theErr = err
- break
- }
- }
-
- if test.err && theErr == nil {
- t.Fatalf("expected error but did not get one")
- } else if !test.err && theErr != nil {
- t.Fatalf("expected no error but got one: %+v", theErr)
- } else if test.err && theErr != nil {
- // test pass...
- return
- }
-
- // ensure there is exactly one entry
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != 1 {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- // get the resulting metadata object
- if actual, err := s.GetVulnerabilityMetadata(test.expected.ID, test.expected.RecordSource); err != nil {
- t.Fatalf("failed to get metadata: %+v", err)
- } else {
- diffs := deep.Equal(&test.expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
- })
- }
-}
diff --git a/grype/db/v2/vulnerability.go b/grype/db/v2/vulnerability.go
deleted file mode 100644
index f76d76f70f5..00000000000
--- a/grype/db/v2/vulnerability.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package v2
-
-// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.
-type Vulnerability struct {
- ID string // The identifier of the vulnerability or advisory
- RecordSource string // The source of the vulnerability information
- PackageName string // The name of the package that is vulnerable
- Namespace string // The ecosystem where the package resides
- VersionConstraint string // The version range which the given package is vulnerable
- VersionFormat string // The format which all version fields should be interpreted as
- CPEs []string // The CPEs which are considered vulnerable
- ProxyVulnerabilities []string // IDs of other Vulnerabilities that are related to this one (this is how advisories relate to CVEs)
- FixedInVersion string // The version which this particular vulnerability was fixed in
-}
diff --git a/grype/db/v2/vulnerability_metadata.go b/grype/db/v2/vulnerability_metadata.go
deleted file mode 100644
index d92395b5e67..00000000000
--- a/grype/db/v2/vulnerability_metadata.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package v2
-
-// VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching.
-type VulnerabilityMetadata struct {
- ID string // The identifier of the vulnerability or advisory
- RecordSource string // The source of the vulnerability information
- Severity string // How severe the vulnerability is (valid values are defined by upstream sources currently)
- Links []string // URLs to get more information about the vulnerability or advisory
- Description string // Description of the vulnerability
- CvssV2 *Cvss // Common Vulnerability Scoring System V2 values
- CvssV3 *Cvss // Common Vulnerability Scoring System V3 values
-}
-
-// Cvss contains select Common Vulnerability Scoring System fields for a vulnerability.
-type Cvss struct {
- BaseScore float64 // Ranges from 0 - 10 and defines for qualities intrinsic to a vulnerability
- ExploitabilityScore float64 // Indicator of how easy it may be for an attacker to exploit a vulnerability
- ImpactScore float64 // Representation of the effects of an exploited vulnerability relative to compromise in confidentiality, integrity, and availability
- Vector string // A textual representation of the metric values used to determine the score
-}
diff --git a/grype/db/v2/vulnerability_metadata_store.go b/grype/db/v2/vulnerability_metadata_store.go
deleted file mode 100644
index 65b726e03d9..00000000000
--- a/grype/db/v2/vulnerability_metadata_store.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package v2
-
-type VulnerabilityMetadataStore interface {
- VulnerabilityMetadataStoreReader
- VulnerabilityMetadataStoreWriter
-}
-
-type VulnerabilityMetadataStoreReader interface {
- GetVulnerabilityMetadata(id, recordSource string) (*VulnerabilityMetadata, error)
-}
-
-type VulnerabilityMetadataStoreWriter interface {
- AddVulnerabilityMetadata(metadata ...VulnerabilityMetadata) error
-}
diff --git a/grype/db/v2/vulnerability_store.go b/grype/db/v2/vulnerability_store.go
deleted file mode 100644
index d3c18e64a35..00000000000
--- a/grype/db/v2/vulnerability_store.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package v2
-
-const VulnerabilityStoreFileName = "vulnerability.db"
-
-type VulnerabilityStore interface {
- VulnerabilityStoreReader
- VulnerabilityStoreWriter
-}
-
-type VulnerabilityStoreReader interface {
- // GetVulnerability retrieves vulnerabilities associated with a namespace and a package name
- GetVulnerability(namespace, name string) ([]Vulnerability, error)
-}
-
-type VulnerabilityStoreWriter interface {
- // AddVulnerability inserts a new record of a vulnerability into the store
- AddVulnerability(vulnerabilities ...Vulnerability) error
-}
diff --git a/grype/db/v3/advisory.go b/grype/db/v3/advisory.go
deleted file mode 100644
index 2bd4784395f..00000000000
--- a/grype/db/v3/advisory.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package v3
-
-// Advisory represents published statements regarding a vulnerability (and potentially about it's resolution).
-type Advisory struct {
- ID string
- Link string
-}
diff --git a/grype/db/v3/diff.go b/grype/db/v3/diff.go
deleted file mode 100644
index d9d2cc522e3..00000000000
--- a/grype/db/v3/diff.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package v3
-
-type DiffReason = string
-
-const (
- DiffAdded DiffReason = "added"
- DiffChanged DiffReason = "changed"
- DiffRemoved DiffReason = "removed"
-)
-
-type Diff struct {
- Reason DiffReason `json:"reason"`
- ID string `json:"id"`
- Namespace string `json:"namespace"`
- Packages []string `json:"packages"`
-}
diff --git a/grype/db/v3/fix.go b/grype/db/v3/fix.go
deleted file mode 100644
index e7d6269aa85..00000000000
--- a/grype/db/v3/fix.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package v3
-
-type FixState string
-
-const (
- UnknownFixState FixState = "unknown"
- FixedState FixState = "fixed"
- NotFixedState FixState = "not-fixed"
- WontFixState FixState = "wont-fix"
-)
-
-// Fix represents all information about known fixes for a stated vulnerability.
-type Fix struct {
- Versions []string // The version(s) which this particular vulnerability was fixed in
- State FixState
-}
diff --git a/grype/db/v3/id.go b/grype/db/v3/id.go
deleted file mode 100644
index 1a5033e3791..00000000000
--- a/grype/db/v3/id.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package v3
-
-import (
- "time"
-)
-
-// ID represents identifying information for a DB and the data it contains.
-type ID struct {
- // BuildTimestamp is the timestamp used to define the age of the DB, ideally including the age of the data
- // contained in the DB, not just when the DB file was created.
- BuildTimestamp time.Time
- SchemaVersion int
-}
-
-type IDReader interface {
- GetID() (*ID, error)
-}
-
-type IDWriter interface {
- SetID(ID) error
-}
-
-func NewID(age time.Time) ID {
- return ID{
- BuildTimestamp: age.UTC(),
- SchemaVersion: SchemaVersion,
- }
-}
diff --git a/grype/db/v3/namespace.go b/grype/db/v3/namespace.go
deleted file mode 100644
index ab43539ac64..00000000000
--- a/grype/db/v3/namespace.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package v3
-
-import (
- "fmt"
- "strings"
-
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/internal/log"
- "github.com/anchore/grype/internal/stringutil"
- packageurl "github.com/anchore/packageurl-go"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-const (
- NVDNamespace = "nvd"
- MSRCNamespacePrefix = "msrc"
- VulnDBNamespace = "vulndb"
-)
-
-func RecordSource(feed, group string) string {
- return fmt.Sprintf("%s:%s", feed, group)
-}
-
-func NamespaceForFeedGroup(feed, group string) (string, error) {
- switch {
- case feed == "vulnerabilities":
- return group, nil
- case feed == "github":
- return group, nil
- case feed == "nvdv2" && group == "nvdv2:cves":
- return NVDNamespace, nil
- case feed == "vulndb" && group == "vulndb:vulnerabilities":
- return VulnDBNamespace, nil
- case feed == "microsoft" && strings.HasPrefix(group, MSRCNamespacePrefix+":"):
- return group, nil
- }
- return "", fmt.Errorf("feed=%q group=%q has no namespace mappings", feed, group)
-}
-
-// NamespaceFromDistro returns the correct Feed Service namespace for the given
-// distro. A namespace is a distinct identifier from the Feed Service, and it
-// can be a combination of distro name and version(s), for example "amzn:8".
-// This is critical to query the database and correlate the distro version with
-// feed contents. Namespaces have to exist in the Feed Service, otherwise,
-// this causes no results to be returned when the database is queried.
-func NamespaceForDistro(d *distro.Distro) string {
- if d == nil {
- return ""
- }
-
- if d.IsRolling() {
- return fmt.Sprintf("%s:%s", strings.ToLower(d.Type.String()), "rolling")
- }
-
- var versionSegments []int
- if d.Version != nil {
- versionSegments = d.Version.Segments()
- }
-
- if len(versionSegments) > 0 {
- switch d.Type {
- // derived from https://github.com/anchore/anchore-engine/blob/5bbbe6b9744f2fb806198ae5d6f0cfe3b367fd9d/anchore_engine/services/policy_engine/__init__.py#L149-L159
- case distro.CentOS, distro.RedHat, distro.Fedora, distro.RockyLinux, distro.AlmaLinux:
- // TODO: there is no mapping of fedora version to RHEL latest version (only the name)
- return fmt.Sprintf("rhel:%d", versionSegments[0])
- case distro.AmazonLinux:
- return fmt.Sprintf("amzn:%d", versionSegments[0])
- case distro.OracleLinux:
- return fmt.Sprintf("ol:%d", versionSegments[0])
- case distro.Alpine:
- // XXX this assumes that a major and minor versions will always exist in Segments
- return fmt.Sprintf("alpine:%d.%d", versionSegments[0], versionSegments[1])
- case distro.SLES:
- return fmt.Sprintf("sles:%d.%d", versionSegments[0], versionSegments[1])
- case distro.Windows:
- return fmt.Sprintf("%s:%d", MSRCNamespacePrefix, versionSegments[0])
- }
- }
- return fmt.Sprintf("%s:%s", strings.ToLower(d.Type.String()), d.FullVersion())
-}
-
-func NamespacesIndexedByCPE() []string {
- return []string{NVDNamespace, VulnDBNamespace}
-}
-
-func NamespacePackageNamersForLanguage(l syftPkg.Language) map[string]NamerByPackage {
- namespaces := make(map[string]NamerByPackage)
- switch l {
- case syftPkg.Ruby:
- namespaces["github:gem"] = defaultPackageNamer
- case syftPkg.Java:
- namespaces["github:java"] = githubJavaPackageNamer
- case syftPkg.JavaScript:
- namespaces["github:npm"] = defaultPackageNamer
- case syftPkg.Python:
- namespaces["github:python"] = defaultPackageNamer
- case syftPkg.Dotnet:
- namespaces["github:nuget"] = defaultPackageNamer
- default:
- namespaces[fmt.Sprintf("github:%s", l)] = defaultPackageNamer
- }
- return namespaces
-}
-
-type NamerByPackage func(p pkg.Package) []string
-
-func defaultPackageNamer(p pkg.Package) []string {
- return []string{p.Name}
-}
-
-func githubJavaPackageNamer(p pkg.Package) []string {
- names := stringutil.NewStringSet()
-
- // all github advisories are stored by ":"
- if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok {
- if metadata.PomGroupID != "" {
- if metadata.PomArtifactID != "" {
- names.Add(fmt.Sprintf("%s:%s", metadata.PomGroupID, metadata.PomArtifactID))
- }
- if metadata.ManifestName != "" {
- names.Add(fmt.Sprintf("%s:%s", metadata.PomGroupID, metadata.ManifestName))
- }
- }
- }
-
- if p.PURL != "" {
- purl, err := packageurl.FromString(p.PURL)
- if err != nil {
- log.Warnf("unable to extract GHSA java package information from purl=%q: %+v", p.PURL, err)
- } else {
- names.Add(fmt.Sprintf("%s:%s", purl.Namespace, purl.Name))
- }
- }
-
- return names.ToSlice()
-}
diff --git a/grype/db/v3/namespace_test.go b/grype/db/v3/namespace_test.go
deleted file mode 100644
index 9e67aa8a36f..00000000000
--- a/grype/db/v3/namespace_test.go
+++ /dev/null
@@ -1,523 +0,0 @@
-package v3
-
-import (
- "fmt"
- "testing"
-
- "github.com/google/uuid"
- "github.com/scylladb/go-set/strset"
- "github.com/stretchr/testify/assert"
-
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/pkg"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-func Test_NamespaceFromRecordSource(t *testing.T) {
- tests := []struct {
- Feed, Group string
- Namespace string
- }{
- {
- Feed: "vulnerabilities",
- Group: "ubuntu:20.04",
- Namespace: "ubuntu:20.04",
- },
- {
- Feed: "vulnerabilities",
- Group: "alpine:3.9",
- Namespace: "alpine:3.9",
- },
- {
- Feed: "nvdv2",
- Group: "nvdv2:cves",
- Namespace: "nvd",
- },
- {
- Feed: "github",
- Group: "github:python",
- Namespace: "github:python",
- },
- {
- Feed: "vulndb",
- Group: "vulndb:vulnerabilities",
- Namespace: "vulndb",
- },
- {
- Feed: "microsoft",
- Group: "msrc:11769",
- Namespace: "msrc:11769",
- },
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("feed=%q group=%q namespace=%q", test.Feed, test.Group, test.Namespace), func(t *testing.T) {
- actual, err := NamespaceForFeedGroup(test.Feed, test.Group)
- assert.NoError(t, err)
- assert.Equal(t, test.Namespace, actual)
- })
- }
-}
-
-func Test_NamespaceForDistro(t *testing.T) {
- tests := []struct {
- dist distro.Type
- version string
- expected string
- }{
- // regression: https://github.com/anchore/grype/issues/221
- {
- dist: distro.RedHat,
- version: "8.3",
- expected: "rhel:8",
- },
- {
- dist: distro.CentOS,
- version: "8.3",
- expected: "rhel:8",
- },
- {
- dist: distro.AmazonLinux,
- version: "8.3",
- expected: "amzn:8",
- },
- {
- dist: distro.OracleLinux,
- version: "8.3",
- expected: "ol:8",
- },
- {
- dist: distro.Fedora,
- version: "31.1",
- // TODO: this is incorrect and will be solved in a future issue (to map the fedora version to the rhel latest version)
- expected: "rhel:31",
- },
- // end of regression #221
- {
- dist: distro.RedHat,
- version: "8",
- expected: "rhel:8",
- },
- {
- dist: distro.AmazonLinux,
- version: "2",
- expected: "amzn:2",
- },
- {
- dist: distro.OracleLinux,
- version: "6",
- expected: "ol:6",
- },
- {
- dist: distro.Alpine,
- version: "1.3.1",
- expected: "alpine:1.3",
- },
- {
- dist: distro.Debian,
- version: "8",
- expected: "debian:8",
- },
- {
- dist: distro.Fedora,
- version: "31",
- expected: "rhel:31",
- },
- {
- dist: distro.Busybox,
- version: "3.1.1",
- expected: "busybox:3.1.1",
- },
- {
- dist: distro.CentOS,
- version: "7",
- expected: "rhel:7",
- },
- {
- dist: distro.Ubuntu,
- version: "18.04",
- expected: "ubuntu:18.04",
- },
- {
- // TODO: this is not correct. This should be mapped to a feed source.
- dist: distro.ArchLinux,
- version: "", // ArchLinux doesn't expose a version
- expected: "archlinux:rolling",
- },
- {
- // TODO: this is not correct. This should be mapped to a feed source.
- dist: distro.OpenSuseLeap,
- version: "15.2",
- expected: "opensuseleap:15.2",
- },
- {
- // TODO: this is not correct. This should be mapped to a feed source.
- dist: distro.Photon,
- version: "4.0",
- expected: "photon:4.0",
- },
- {
- dist: distro.SLES,
- version: "12.5",
- expected: "sles:12.5",
- },
- {
- dist: distro.Windows,
- version: "471816",
- expected: "msrc:471816",
- },
- {
- dist: distro.RockyLinux,
- version: "8.5",
- expected: "rhel:8",
- },
- {
- dist: distro.AlmaLinux,
- version: "8.5",
- expected: "rhel:8",
- },
- {
- dist: distro.Gentoo,
- version: "", // Gentoo is a rolling release
- expected: "gentoo:rolling",
- },
- {
- dist: distro.Wolfi,
- version: "2022yzblah", // Wolfi is a rolling release
- expected: "wolfi:rolling",
- },
- {
- dist: distro.Chainguard,
- expected: "chainguard:rolling",
- },
- }
-
- observedDistros := strset.New()
- allDistros := strset.New()
-
- for _, d := range distro.All {
- allDistros.Add(d.String())
- }
-
- // TODO: what do we do with mariner
- allDistros.Remove(distro.Mariner.String())
-
- for _, test := range tests {
- name := fmt.Sprintf("%s:%s", test.dist, test.version)
- t.Run(name, func(t *testing.T) {
- d, err := distro.New(test.dist, test.version, "")
- assert.NoError(t, err)
- observedDistros.Add(d.Type.String())
- assert.Equal(t, test.expected, NamespaceForDistro(d))
- })
- }
-
- assert.ElementsMatch(t, allDistros.List(), observedDistros.List(), "at least one distro doesn't have a corresponding test")
-}
-
-func Test_NamespacesIndexedByCPE(t *testing.T) {
- assert.ElementsMatch(t, NamespacesIndexedByCPE(), []string{"nvd", "vulndb"})
-}
-
-func Test_NamespacesForLanguage(t *testing.T) {
- tests := []struct {
- language syftPkg.Language
- namerInput *pkg.Package
- expectedNamespaces []string
- expectedNames []string
- }{
- // default languages
- {
- language: syftPkg.Rust,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- },
- expectedNamespaces: []string{
- "github:rust",
- },
- expectedNames: []string{
- "a-name",
- },
- },
- {
- language: syftPkg.Go,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- },
- expectedNamespaces: []string{
- "github:go",
- },
- expectedNames: []string{
- "a-name",
- },
- },
- // supported languages
- {
- language: syftPkg.Ruby,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- },
- expectedNamespaces: []string{
- "github:gem",
- },
- expectedNames: []string{
- "a-name",
- },
- },
- {
- language: syftPkg.JavaScript,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- },
- expectedNamespaces: []string{
- "github:npm",
- },
- expectedNames: []string{
- "a-name",
- },
- },
- {
- language: syftPkg.Python,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- },
- expectedNamespaces: []string{
- "github:python",
- },
- expectedNames: []string{
- "a-name",
- },
- },
- {
- language: syftPkg.Java,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- Metadata: pkg.JavaMetadata{
- VirtualPath: "v-path",
- PomArtifactID: "art-id",
- PomGroupID: "g-id",
- ManifestName: "man-name",
- },
- },
- expectedNamespaces: []string{
- "github:java",
- },
- expectedNames: []string{
- "g-id:art-id",
- "g-id:man-name",
- },
- },
- {
- language: syftPkg.Dart,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- },
- expectedNamespaces: []string{
- "github:dart",
- },
- expectedNames: []string{
- "a-name",
- },
- },
- {
- language: syftPkg.Dotnet,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- },
- expectedNamespaces: []string{
- "github:nuget",
- },
- expectedNames: []string{
- "a-name",
- },
- },
- {
- language: syftPkg.Haskell,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "h-name",
- },
- expectedNamespaces: []string{
- "github:haskell",
- },
- expectedNames: []string{
- "h-name",
- },
- },
- {
- language: syftPkg.Elixir,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "e-name",
- },
- expectedNamespaces: []string{
- "github:elixir",
- },
- expectedNames: []string{
- "e-name",
- },
- },
- {
- language: syftPkg.Erlang,
- namerInput: &pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "2-name",
- },
- expectedNamespaces: []string{
- "github:erlang",
- },
- expectedNames: []string{
- "2-name",
- },
- },
- }
-
- observedLanguages := strset.New()
- allLanguages := strset.New()
-
- for _, l := range syftPkg.AllLanguages {
- allLanguages.Add(string(l))
- }
-
- // remove PHP, CPP for coverage as feed has not been updated
- allLanguages.Remove(string(syftPkg.PHP))
- allLanguages.Remove(string(syftPkg.CPP))
- allLanguages.Remove(string(syftPkg.Swift))
- allLanguages.Remove(string(syftPkg.R))
-
- for _, test := range tests {
- t.Run(string(test.language), func(t *testing.T) {
- observedLanguages.Add(string(test.language))
- var actualNamespaces, actualNames []string
- namers := NamespacePackageNamersForLanguage(test.language)
- for namespace, namerFn := range namers {
- actualNamespaces = append(actualNamespaces, namespace)
- actualNames = append(actualNames, namerFn(*test.namerInput)...)
- }
- assert.ElementsMatch(t, actualNamespaces, test.expectedNamespaces)
- assert.ElementsMatch(t, actualNames, test.expectedNames)
- })
- }
-
- assert.ElementsMatch(t, allLanguages.List(), observedLanguages.List(), "at least one language doesn't have a corresponding test")
-}
-
-func Test_githubJavaPackageNamer(t *testing.T) {
- tests := []struct {
- name string
- namerInput pkg.Package
- expected []string
- }{
- {
- name: "both artifact and manifest",
- namerInput: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- Metadata: pkg.JavaMetadata{
- VirtualPath: "v-path",
- PomArtifactID: "art-id",
- PomGroupID: "g-id",
- ManifestName: "man-name",
- },
- },
- expected: []string{
- "g-id:art-id",
- "g-id:man-name",
- },
- },
- {
- name: "no group id",
- namerInput: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- Metadata: pkg.JavaMetadata{
- VirtualPath: "v-path",
- PomArtifactID: "art-id",
- ManifestName: "man-name",
- },
- },
- expected: []string{},
- },
- {
- name: "only manifest",
- namerInput: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- Metadata: pkg.JavaMetadata{
- VirtualPath: "v-path",
- PomGroupID: "g-id",
- ManifestName: "man-name",
- },
- },
- expected: []string{
- "g-id:man-name",
- },
- },
- {
- name: "only artifact",
- namerInput: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- Metadata: pkg.JavaMetadata{
- VirtualPath: "v-path",
- PomArtifactID: "art-id",
- PomGroupID: "g-id",
- },
- },
- expected: []string{
- "g-id:art-id",
- },
- },
- {
- name: "no artifact or manifest",
- namerInput: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- Metadata: pkg.JavaMetadata{
- VirtualPath: "v-path",
- PomGroupID: "g-id",
- },
- },
- expected: []string{},
- },
- {
- name: "with valid purl",
- namerInput: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- PURL: "pkg:maven/org.anchore/b-name@0.2",
- },
- expected: []string{"org.anchore:b-name"},
- },
- {
- name: "ignore invalid pURLs",
- namerInput: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "a-name",
- PURL: "pkg:BAD/",
- Metadata: pkg.JavaMetadata{
- VirtualPath: "v-path",
- PomArtifactID: "art-id",
- PomGroupID: "g-id",
- },
- },
- expected: []string{
- "g-id:art-id",
- },
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- assert.ElementsMatch(t, githubJavaPackageNamer(test.namerInput), test.expected)
- })
- }
-}
diff --git a/grype/db/v3/schema_version.go b/grype/db/v3/schema_version.go
deleted file mode 100644
index 5a3e5194e6c..00000000000
--- a/grype/db/v3/schema_version.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package v3
-
-const SchemaVersion = 3
diff --git a/grype/db/v3/store.go b/grype/db/v3/store.go
deleted file mode 100644
index 0817651c651..00000000000
--- a/grype/db/v3/store.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package v3
-
-type Store interface {
- StoreReader
- StoreWriter
-}
-
-type StoreReader interface {
- IDReader
- DiffReader
- VulnerabilityStoreReader
- VulnerabilityMetadataStoreReader
-}
-
-type StoreWriter interface {
- IDWriter
- VulnerabilityStoreWriter
- VulnerabilityMetadataStoreWriter
- Close()
-}
-
-type DiffReader interface {
- DiffStore(s StoreReader) (*[]Diff, error)
-}
diff --git a/grype/db/v3/store/diff.go b/grype/db/v3/store/diff.go
deleted file mode 100644
index ff32486818b..00000000000
--- a/grype/db/v3/store/diff.go
+++ /dev/null
@@ -1,305 +0,0 @@
-package store
-
-import (
- "github.com/wagoodman/go-partybus"
- "github.com/wagoodman/go-progress"
-
- v3 "github.com/anchore/grype/grype/db/v3"
- "github.com/anchore/grype/grype/event"
- "github.com/anchore/grype/grype/event/monitor"
- "github.com/anchore/grype/internal/bus"
-)
-
-type storeKey struct {
- id string
- namespace string
- packageName string
-}
-
-type PkgMap = map[storeKey][]string
-
-type storeVulnerabilityList struct {
- items map[storeKey][]storeVulnerability
- seen bool
-}
-type storeVulnerability struct {
- item *v3.Vulnerability
- seen bool
-}
-type storeMetadata struct {
- item *v3.VulnerabilityMetadata
- seen bool
-}
-
-// create manual progress bars for tracking the database diff's progress
-func trackDiff(total int64) (*progress.Manual, *progress.Manual, *progress.Stage) {
- stageProgress := &progress.Manual{}
- stageProgress.SetTotal(total)
- differencesDiscovered := &progress.Manual{}
- stager := &progress.Stage{}
-
- bus.Publish(partybus.Event{
- Type: event.DatabaseDiffingStarted,
- Value: monitor.DBDiff{
- Stager: stager,
- StageProgress: progress.Progressable(stageProgress),
- DifferencesDiscovered: progress.Monitorable(differencesDiscovered),
- },
- })
- return stageProgress, differencesDiscovered, stager
-}
-
-// creates a map from an unpackaged key to a list of all packages associated with it
-func buildVulnerabilityPkgsMap(models *[]v3.Vulnerability) *map[storeKey][]string {
- storeMap := make(map[storeKey][]string)
- for _, m := range *models {
- model := m
- k := getVulnerabilityParentKey(model)
- if storeVuln, exists := storeMap[k]; exists {
- storeMap[k] = append(storeVuln, model.PackageName)
- } else {
- storeMap[k] = []string{model.PackageName}
- }
- }
- return &storeMap
-}
-
-// creates a diff from the given key using the package maps information to populate
-// the relevant packages affected by the update
-func createDiff(baseStore, targetStore *PkgMap, key storeKey, reason v3.DiffReason) *v3.Diff {
- pkgMap := make(map[string]struct{})
-
- key.packageName = ""
- if baseStore != nil {
- if basePkgs, exists := (*baseStore)[key]; exists {
- for _, pkg := range basePkgs {
- pkgMap[pkg] = struct{}{}
- }
- }
- }
- if targetStore != nil {
- if targetPkgs, exists := (*targetStore)[key]; exists {
- for _, pkg := range targetPkgs {
- pkgMap[pkg] = struct{}{}
- }
- }
- }
- pkgs := []string{}
- for pkg := range pkgMap {
- pkgs = append(pkgs, pkg)
- }
-
- return &v3.Diff{
- Reason: reason,
- ID: key.id,
- Namespace: key.namespace,
- Packages: pkgs,
- }
-}
-
-// gets an unpackaged key from a vulnerability
-func getVulnerabilityParentKey(vuln v3.Vulnerability) storeKey {
- return storeKey{vuln.ID, vuln.Namespace, ""}
-}
-
-// gets a packaged key from a vulnerability
-func getVulnerabilityKey(vuln v3.Vulnerability) storeKey {
- return storeKey{vuln.ID, vuln.Namespace, vuln.PackageName}
-}
-
-type VulnerabilitySet struct {
- data map[storeKey]*storeVulnerabilityList
-}
-
-func NewVulnerabilitySet(models *[]v3.Vulnerability) *VulnerabilitySet {
- m := make(map[storeKey]*storeVulnerabilityList, len(*models))
- for _, mm := range *models {
- model := mm
- parentKey := getVulnerabilityParentKey(model)
- vulnKey := getVulnerabilityKey(model)
- if storeVuln, exists := m[parentKey]; exists {
- if kk, exists := storeVuln.items[vulnKey]; exists {
- storeVuln.items[vulnKey] = append(kk, storeVulnerability{
- item: &model,
- seen: false,
- })
- } else {
- storeVuln.items[vulnKey] = []storeVulnerability{{&model, false}}
- }
- } else {
- vuln := storeVulnerabilityList{
- items: make(map[storeKey][]storeVulnerability),
- seen: false,
- }
- vuln.items[vulnKey] = []storeVulnerability{{&model, false}}
- m[parentKey] = &vuln
- }
- }
- return &VulnerabilitySet{
- data: m,
- }
-}
-
-func (v *VulnerabilitySet) in(item v3.Vulnerability) bool {
- _, exists := v.data[getVulnerabilityParentKey(item)]
- return exists
-}
-
-func (v *VulnerabilitySet) match(item v3.Vulnerability) bool {
- if parent, exists := v.data[getVulnerabilityParentKey(item)]; exists {
- parent.seen = true
- key := getVulnerabilityKey(item)
- if children, exists := parent.items[key]; exists {
- for idx, child := range children {
- if item.Equal(*child.item) {
- children[idx].seen = true
- return true
- }
- }
- }
- }
- return false
-}
-
-func (v *VulnerabilitySet) getUnmatched() ([]storeKey, []storeKey) {
- notSeen := []storeKey{}
- notEntirelySeen := []storeKey{}
- for k, item := range v.data {
- if !item.seen {
- notSeen = append(notSeen, k)
- continue
- }
- componentLoop:
- for _, components := range item.items {
- for _, component := range components {
- if !component.seen {
- notEntirelySeen = append(notEntirelySeen, k)
- break componentLoop
- }
- }
- }
- }
- return notSeen, notEntirelySeen
-}
-
-func diffVulnerabilities(baseModels, targetModels *[]v3.Vulnerability, basePkgsMap, targetPkgsMap *PkgMap, differentItems *progress.Manual) *map[string]*v3.Diff {
- diffs := make(map[string]*v3.Diff)
- m := NewVulnerabilitySet(baseModels)
-
- for _, tModel := range *targetModels {
- targetModel := tModel
- k := getVulnerabilityKey(targetModel)
- if m.in(targetModel) {
- matched := m.match(targetModel)
- if !matched {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v3.DiffChanged)
- differentItems.Increment()
- }
- } else {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(nil, targetPkgsMap, k, v3.DiffAdded)
- differentItems.Increment()
- }
- }
- notSeen, partialSeen := m.getUnmatched()
- for _, k := range partialSeen {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v3.DiffChanged)
- differentItems.Increment()
- }
- for _, k := range notSeen {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, nil, k, v3.DiffRemoved)
- differentItems.Increment()
- }
-
- return &diffs
-}
-
-type MetadataSet struct {
- data map[storeKey]*storeMetadata
-}
-
-func NewMetadataSet(models *[]v3.VulnerabilityMetadata) *MetadataSet {
- m := make(map[storeKey]*storeMetadata, len(*models))
- for _, mm := range *models {
- model := mm
- m[getMetadataKey(model)] = &storeMetadata{
- item: &model,
- seen: false,
- }
- }
- return &MetadataSet{
- data: m,
- }
-}
-
-func (v *MetadataSet) in(item v3.VulnerabilityMetadata) bool {
- _, exists := v.data[getMetadataKey(item)]
- return exists
-}
-
-func (v *MetadataSet) match(item v3.VulnerabilityMetadata) bool {
- if baseModel, exists := v.data[getMetadataKey(item)]; exists {
- baseModel.seen = true
- return baseModel.item.Equal(item)
- }
- return false
-}
-
-func (v *MetadataSet) getUnmatched() []storeKey {
- notSeen := []storeKey{}
- for k, item := range v.data {
- if !item.seen {
- notSeen = append(notSeen, k)
- }
- }
- return notSeen
-}
-
-func diffVulnerabilityMetadata(baseModels, targetModels *[]v3.VulnerabilityMetadata, basePkgsMap, targetPkgsMap *PkgMap, differentItems *progress.Manual) *map[string]*v3.Diff {
- diffs := make(map[string]*v3.Diff)
- m := NewMetadataSet(baseModels)
-
- for _, tModel := range *targetModels {
- targetModel := tModel
- k := getMetadataKey(targetModel)
- if m.in(targetModel) {
- if !m.match(targetModel) {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v3.DiffChanged)
- differentItems.Increment()
- }
- } else {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(nil, targetPkgsMap, k, v3.DiffAdded)
- differentItems.Increment()
- }
- }
- for _, k := range m.getUnmatched() {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, nil, k, v3.DiffRemoved)
- differentItems.Increment()
- }
-
- return &diffs
-}
-
-func getMetadataKey(metadata v3.VulnerabilityMetadata) storeKey {
- return storeKey{metadata.ID, metadata.Namespace, ""}
-}
diff --git a/grype/db/v3/store/diff_test.go b/grype/db/v3/store/diff_test.go
deleted file mode 100644
index dede9a28aaa..00000000000
--- a/grype/db/v3/store/diff_test.go
+++ /dev/null
@@ -1,236 +0,0 @@
-package store
-
-import (
- "os"
- "sort"
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- v3 "github.com/anchore/grype/grype/db/v3"
-)
-
-func Test_GetAllVulnerabilities(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- //WHEN
- result, err := s.GetAllVulnerabilities()
-
- //THEN
- assert.NotNil(t, result)
- assert.NoError(t, err)
-}
-
-func Test_GetAllVulnerabilityMetadata(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- //WHEN
- result, err := s.GetAllVulnerabilityMetadata()
-
- //THEN
- assert.NotNil(t, result)
- assert.NoError(t, err)
-}
-
-func Test_Diff_Vulnerabilities(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
-
- s1, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
- dbTempFile = t.TempDir()
- defer os.Remove(dbTempFile)
-
- s2, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- baseVulns := []v3.Vulnerability{
- {
- Namespace: "github:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 2.0 >= 1.29",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "github:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- PackageName: "npm:axios",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"},
- Fix: v3.Fix{
- State: v3.UnknownFixState,
- },
- },
- }
- targetVulns := []v3.Vulnerability{
- {
- Namespace: "github:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 2.0 >= 1.29",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "github:go",
- ID: "GHSA-....-....",
- PackageName: "hashicorp:nomad",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:golang:hashicorp:nomad:*:*:*:*:*"},
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- PackageName: "npm:axios",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"},
- Fix: v3.Fix{
- State: v3.WontFixState,
- },
- },
- }
- expectedDiffs := []v3.Diff{
- {
- Reason: v3.DiffChanged,
- ID: "CVE-123-4567",
- Namespace: "github:python",
- Packages: []string{"pypi:requests"},
- },
- {
- Reason: v3.DiffChanged,
- ID: "CVE-123-7654",
- Namespace: "npm",
- Packages: []string{"npm:axios"},
- },
- {
- Reason: v3.DiffAdded,
- ID: "GHSA-....-....",
- Namespace: "github:go",
- Packages: []string{"hashicorp:nomad"},
- },
- }
-
- for _, vuln := range baseVulns {
- s1.AddVulnerability(vuln)
- }
- for _, vuln := range targetVulns {
- s2.AddVulnerability(vuln)
- }
-
- //WHEN
- result, err := s1.DiffStore(s2)
- sort.SliceStable(*result, func(i, j int) bool {
- return (*result)[i].ID < (*result)[j].ID
- })
-
- //THEN
- assert.NoError(t, err)
- assert.Equal(t, expectedDiffs, *result)
-}
-
-func Test_Diff_Metadata(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
-
- s1, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
- dbTempFile = t.TempDir()
-
- s2, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- baseVulns := []v3.VulnerabilityMetadata{
- {
- Namespace: "github:python",
- ID: "CVE-123-4567",
- DataSource: "nvd",
- },
- {
- Namespace: "github:python",
- ID: "CVE-123-4567",
- DataSource: "nvd",
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- DataSource: "nvd",
- },
- }
- targetVulns := []v3.VulnerabilityMetadata{
- {
- Namespace: "github:go",
- ID: "GHSA-....-....",
- DataSource: "nvd",
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- DataSource: "vulndb",
- },
- }
- expectedDiffs := []v3.Diff{
- {
- Reason: v3.DiffRemoved,
- ID: "CVE-123-4567",
- Namespace: "github:python",
- Packages: []string{},
- },
- {
- Reason: v3.DiffChanged,
- ID: "CVE-123-7654",
- Namespace: "npm",
- Packages: []string{},
- },
- {
- Reason: v3.DiffAdded,
- ID: "GHSA-....-....",
- Namespace: "github:go",
- Packages: []string{},
- },
- }
-
- for _, vuln := range baseVulns {
- s1.AddVulnerabilityMetadata(vuln)
- }
- for _, vuln := range targetVulns {
- s2.AddVulnerabilityMetadata(vuln)
- }
-
- //WHEN
- result, err := s1.DiffStore(s2)
-
- //THEN
- sort.SliceStable(*result, func(i, j int) bool {
- return (*result)[i].ID < (*result)[j].ID
- })
-
- assert.NoError(t, err)
- assert.Equal(t, expectedDiffs, *result)
-}
diff --git a/grype/db/v3/store/model/id.go b/grype/db/v3/store/model/id.go
deleted file mode 100644
index 956de022709..00000000000
--- a/grype/db/v3/store/model/id.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package model
-
-import (
- "fmt"
- "time"
-
- v3 "github.com/anchore/grype/grype/db/v3"
-)
-
-const (
- IDTableName = "id"
-)
-
-type IDModel struct {
- BuildTimestamp string `gorm:"column:build_timestamp"`
- SchemaVersion int `gorm:"column:schema_version"`
-}
-
-func NewIDModel(id v3.ID) IDModel {
- return IDModel{
- BuildTimestamp: id.BuildTimestamp.Format(time.RFC3339Nano),
- SchemaVersion: id.SchemaVersion,
- }
-}
-
-func (IDModel) TableName() string {
- return IDTableName
-}
-
-func (m *IDModel) Inflate() (v3.ID, error) {
- buildTime, err := time.Parse(time.RFC3339Nano, m.BuildTimestamp)
- if err != nil {
- return v3.ID{}, fmt.Errorf("unable to parse build timestamp (%+v): %w", m.BuildTimestamp, err)
- }
-
- return v3.ID{
- BuildTimestamp: buildTime,
- SchemaVersion: m.SchemaVersion,
- }, nil
-}
diff --git a/grype/db/v3/store/model/vulnerability.go b/grype/db/v3/store/model/vulnerability.go
deleted file mode 100644
index 263661b5e93..00000000000
--- a/grype/db/v3/store/model/vulnerability.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package model
-
-import (
- "encoding/json"
- "fmt"
-
- v3 "github.com/anchore/grype/grype/db/v3"
-)
-
-const (
- VulnerabilityTableName = "vulnerability"
- GetVulnerabilityIndexName = "get_vulnerability_index"
-)
-
-// VulnerabilityModel is a struct used to serialize db.Vulnerability information into a sqlite3 DB.
-type VulnerabilityModel struct {
- PK uint64 `gorm:"primary_key;auto_increment;"`
- ID string `gorm:"column:id"`
- PackageName string `gorm:"column:package_name; index:get_vulnerability_index"`
- Namespace string `gorm:"column:namespace; index:get_vulnerability_index"`
- VersionConstraint string `gorm:"column:version_constraint"`
- VersionFormat string `gorm:"column:version_format"`
- CPEs string `gorm:"column:cpes"`
- RelatedVulnerabilities string `gorm:"column:related_vulnerabilities"`
- FixedInVersions string `gorm:"column:fixed_in_versions"`
- FixState string `gorm:"column:fix_state"`
- Advisories string `gorm:"column:advisories"`
-}
-
-// NewVulnerabilityModel generates a new model from a db.Vulnerability struct.
-func NewVulnerabilityModel(vulnerability v3.Vulnerability) VulnerabilityModel {
- cpes, err := json.Marshal(vulnerability.CPEs)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- related, err := json.Marshal(vulnerability.RelatedVulnerabilities)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- advisories, err := json.Marshal(vulnerability.Advisories)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- fixedInVersions, err := json.Marshal(vulnerability.Fix.Versions)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- return VulnerabilityModel{
- ID: vulnerability.ID,
- PackageName: vulnerability.PackageName,
- Namespace: vulnerability.Namespace,
- VersionConstraint: vulnerability.VersionConstraint,
- VersionFormat: vulnerability.VersionFormat,
- FixedInVersions: string(fixedInVersions),
- FixState: string(vulnerability.Fix.State),
- Advisories: string(advisories),
- CPEs: string(cpes),
- RelatedVulnerabilities: string(related),
- }
-}
-
-// TableName returns the table which all db.Vulnerability model instances are stored into.
-func (VulnerabilityModel) TableName() string {
- return VulnerabilityTableName
-}
-
-// Inflate generates a db.Vulnerability object from the serialized model instance.
-func (m *VulnerabilityModel) Inflate() (v3.Vulnerability, error) {
- var cpes []string
- err := json.Unmarshal([]byte(m.CPEs), &cpes)
- if err != nil {
- return v3.Vulnerability{}, fmt.Errorf("unable to unmarshal CPEs (%+v): %w", m.CPEs, err)
- }
-
- var related []v3.VulnerabilityReference
- err = json.Unmarshal([]byte(m.RelatedVulnerabilities), &related)
- if err != nil {
- return v3.Vulnerability{}, fmt.Errorf("unable to unmarshal related vulnerabilities (%+v): %w", m.RelatedVulnerabilities, err)
- }
-
- var advisories []v3.Advisory
- err = json.Unmarshal([]byte(m.Advisories), &advisories)
- if err != nil {
- return v3.Vulnerability{}, fmt.Errorf("unable to unmarshal advisories (%+v): %w", m.Advisories, err)
- }
-
- var versions []string
- err = json.Unmarshal([]byte(m.FixedInVersions), &versions)
- if err != nil {
- return v3.Vulnerability{}, fmt.Errorf("unable to unmarshal versions (%+v): %w", m.FixedInVersions, err)
- }
-
- return v3.Vulnerability{
- ID: m.ID,
- PackageName: m.PackageName,
- Namespace: m.Namespace,
- VersionConstraint: m.VersionConstraint,
- VersionFormat: m.VersionFormat,
- CPEs: cpes,
- RelatedVulnerabilities: related,
- Fix: v3.Fix{
- Versions: versions,
- State: v3.FixState(m.FixState),
- },
- Advisories: advisories,
- }, nil
-}
diff --git a/grype/db/v3/store/model/vulnerability_metadata.go b/grype/db/v3/store/model/vulnerability_metadata.go
deleted file mode 100644
index b8c65d52098..00000000000
--- a/grype/db/v3/store/model/vulnerability_metadata.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package model
-
-import (
- "encoding/json"
- "fmt"
-
- v3 "github.com/anchore/grype/grype/db/v3"
-)
-
-const (
- VulnerabilityMetadataTableName = "vulnerability_metadata"
-)
-
-// VulnerabilityMetadataModel is a struct used to serialize db.VulnerabilityMetadata information into a sqlite3 DB.
-type VulnerabilityMetadataModel struct {
- ID string `gorm:"primary_key; column:id;"`
- Namespace string `gorm:"primary_key; column:namespace;"`
- DataSource string `gorm:"column:data_source"`
- RecordSource string `gorm:"column:record_source"`
- Severity string `gorm:"column:severity"`
- URLs string `gorm:"column:urls"`
- Description string `gorm:"column:description"`
- Cvss string `gorm:"column:cvss"`
-}
-
-// NewVulnerabilityMetadataModel generates a new model from a db.VulnerabilityMetadata struct.
-func NewVulnerabilityMetadataModel(metadata v3.VulnerabilityMetadata) VulnerabilityMetadataModel {
- links, err := json.Marshal(metadata.URLs)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- if metadata.Cvss == nil {
- metadata.Cvss = make([]v3.Cvss, 0)
- }
- var cvssStr string
- cvss, err := json.Marshal(metadata.Cvss)
- if err != nil {
- // TODO: just no
- panic(err)
- }
-
- cvssStr = string(cvss)
-
- return VulnerabilityMetadataModel{
- ID: metadata.ID,
- Namespace: metadata.Namespace,
- DataSource: metadata.DataSource,
- RecordSource: metadata.RecordSource,
- Severity: metadata.Severity,
- URLs: string(links),
- Description: metadata.Description,
- Cvss: cvssStr,
- }
-}
-
-// TableName returns the table which all db.VulnerabilityMetadata model instances are stored into.
-func (VulnerabilityMetadataModel) TableName() string {
- return VulnerabilityMetadataTableName
-}
-
-// Inflate generates a db.VulnerabilityMetadataModel object from the serialized model instance.
-func (m *VulnerabilityMetadataModel) Inflate() (v3.VulnerabilityMetadata, error) {
- var links []string
- var cvss []v3.Cvss
-
- if err := json.Unmarshal([]byte(m.URLs), &links); err != nil {
- return v3.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal URLs (%+v): %w", m.URLs, err)
- }
-
- err := json.Unmarshal([]byte(m.Cvss), &cvss)
- if err != nil {
- return v3.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvss data (%+v): %w", m.Cvss, err)
- }
-
- return v3.VulnerabilityMetadata{
- ID: m.ID,
- Namespace: m.Namespace,
- DataSource: m.DataSource,
- RecordSource: m.RecordSource,
- Severity: m.Severity,
- URLs: links,
- Description: m.Description,
- Cvss: cvss,
- }, nil
-}
diff --git a/grype/db/v3/store/store.go b/grype/db/v3/store/store.go
deleted file mode 100644
index 212c0e9449d..00000000000
--- a/grype/db/v3/store/store.go
+++ /dev/null
@@ -1,308 +0,0 @@
-package store
-
-import (
- "fmt"
- "sort"
-
- "github.com/go-test/deep"
- "gorm.io/gorm"
-
- "github.com/anchore/grype/grype/db/internal/gormadapter"
- v3 "github.com/anchore/grype/grype/db/v3"
- "github.com/anchore/grype/grype/db/v3/store/model"
- "github.com/anchore/grype/internal/stringutil"
- _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
-)
-
-// store holds an instance of the database connection
-type store struct {
- db *gorm.DB
-}
-
-// New creates a new instance of the store.
-func New(dbFilePath string, overwrite bool) (v3.Store, error) {
- db, err := gormadapter.Open(dbFilePath, overwrite)
- if err != nil {
- return nil, err
- }
-
- if overwrite {
- // TODO: automigrate could write to the database,
- // we should be validating the database is the correct database based on the version in the ID table before
- // automigrating
- if err := db.AutoMigrate(&model.IDModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate ID model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityMetadataModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability Metadata model: %w", err)
- }
- }
-
- return &store{
- db: db,
- }, nil
-}
-
-// GetID fetches the metadata about the databases schema version and build time.
-func (s *store) GetID() (*v3.ID, error) {
- var models []model.IDModel
- result := s.db.Find(&models)
- if result.Error != nil {
- return nil, result.Error
- }
-
- switch {
- case len(models) > 1:
- return nil, fmt.Errorf("found multiple DB IDs")
- case len(models) == 1:
- id, err := models[0].Inflate()
- if err != nil {
- return nil, err
- }
- return &id, nil
- }
-
- return nil, nil
-}
-
-// SetID stores the databases schema version and build time.
-func (s *store) SetID(id v3.ID) error {
- var ids []model.IDModel
-
- // replace the existing ID with the given one
- s.db.Find(&ids).Delete(&ids)
-
- m := model.NewIDModel(id)
- result := s.db.Create(&m)
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add id (%d rows affected)", result.RowsAffected)
- }
-
- return result.Error
-}
-
-// GetVulnerability retrieves one or more vulnerabilities given a namespace and package name.
-func (s *store) GetVulnerability(namespace, packageName string) ([]v3.Vulnerability, error) {
- var models []model.VulnerabilityModel
-
- result := s.db.Where("namespace = ? AND package_name = ?", namespace, packageName).Find(&models)
-
- var vulnerabilities = make([]v3.Vulnerability, len(models))
- for idx, m := range models {
- vulnerability, err := m.Inflate()
- if err != nil {
- return nil, err
- }
- vulnerabilities[idx] = vulnerability
- }
-
- return vulnerabilities, result.Error
-}
-
-// AddVulnerability saves one or more vulnerabilities into the sqlite3 store.
-func (s *store) AddVulnerability(vulnerabilities ...v3.Vulnerability) error {
- for _, vulnerability := range vulnerabilities {
- m := model.NewVulnerabilityModel(vulnerability)
-
- result := s.db.Create(&m)
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add vulnerability (%d rows affected)", result.RowsAffected)
- }
- }
- return nil
-}
-
-// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
-func (s *store) GetVulnerabilityMetadata(id, namespace string) (*v3.VulnerabilityMetadata, error) {
- var models []model.VulnerabilityMetadataModel
-
- result := s.db.Where(&model.VulnerabilityMetadataModel{ID: id, Namespace: namespace}).Find(&models)
- if result.Error != nil {
- return nil, result.Error
- }
-
- switch {
- case len(models) > 1:
- return nil, fmt.Errorf("found multiple metadatas for single ID=%q Namespace=%q", id, namespace)
- case len(models) == 1:
- metadata, err := models[0].Inflate()
- if err != nil {
- return nil, err
- }
-
- return &metadata, nil
- }
-
- return nil, nil
-}
-
-// AddVulnerabilityMetadata stores one or more vulnerability metadata models into the sqlite DB.
-//
-//nolint:gocognit
-func (s *store) AddVulnerabilityMetadata(metadata ...v3.VulnerabilityMetadata) error {
- for _, m := range metadata {
- existing, err := s.GetVulnerabilityMetadata(m.ID, m.Namespace)
- if err != nil {
- return fmt.Errorf("failed to verify existing entry: %w", err)
- }
-
- if existing != nil {
- // merge with the existing entry
-
- switch {
- case existing.Severity != m.Severity:
- return fmt.Errorf("existing metadata has mismatched severity (%q!=%q)", existing.Severity, m.Severity)
- case existing.Description != m.Description:
- return fmt.Errorf("existing metadata has mismatched description (%q!=%q)", existing.Description, m.Description)
- }
-
- incoming:
- // go through all incoming CVSS and see if they are already stored.
- // If they exist already in the database then skip adding them,
- // preventing a duplicate
- for _, incomingCvss := range m.Cvss {
- for _, existingCvss := range existing.Cvss {
- if len(deep.Equal(incomingCvss, existingCvss)) == 0 {
- // duplicate found, so incoming CVSS shouldn't get added
- continue incoming
- }
- }
- // a duplicate CVSS entry wasn't found, so append the incoming CVSS
- existing.Cvss = append(existing.Cvss, incomingCvss)
- }
-
- links := stringutil.NewStringSetFromSlice(existing.URLs)
- for _, l := range m.URLs {
- links.Add(l)
- }
-
- existing.URLs = links.ToSlice()
- sort.Strings(existing.URLs)
-
- newModel := model.NewVulnerabilityMetadataModel(*existing)
- result := s.db.Save(&newModel)
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to merge vulnerability metadata (%d rows affected)", result.RowsAffected)
- }
-
- if result.Error != nil {
- return result.Error
- }
- } else {
- // this is a new entry
- newModel := model.NewVulnerabilityMetadataModel(m)
- result := s.db.Create(&newModel)
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add vulnerability metadata (%d rows affected)", result.RowsAffected)
- }
- }
- }
- return nil
-}
-
-// GetAllVulnerabilities gets all vulnerabilities in the database
-func (s *store) GetAllVulnerabilities() (*[]v3.Vulnerability, error) {
- var models []model.VulnerabilityModel
- if result := s.db.Find(&models); result.Error != nil {
- return nil, result.Error
- }
- vulns := make([]v3.Vulnerability, len(models))
- for idx, m := range models {
- vuln, err := m.Inflate()
- if err != nil {
- return nil, err
- }
- vulns[idx] = vuln
- }
- return &vulns, nil
-}
-
-// GetAllVulnerabilityMetadata gets all vulnerability metadata in the database
-func (s *store) GetAllVulnerabilityMetadata() (*[]v3.VulnerabilityMetadata, error) {
- var models []model.VulnerabilityMetadataModel
- if result := s.db.Find(&models); result.Error != nil {
- return nil, result.Error
- }
- metadata := make([]v3.VulnerabilityMetadata, len(models))
- for idx, m := range models {
- data, err := m.Inflate()
- if err != nil {
- return nil, err
- }
- metadata[idx] = data
- }
- return &metadata, nil
-}
-
-// DiffStore creates a diff between the current sql database and the given store
-func (s *store) DiffStore(targetStore v3.StoreReader) (*[]v3.Diff, error) {
- // 7 stages, one for each step of the diff process (stages)
- rowsProgress, diffItems, stager := trackDiff(7)
-
- stager.Current = "reading target vulnerabilities"
- targetVulns, err := targetStore.GetAllVulnerabilities()
- rowsProgress.Increment()
- if err != nil {
- return nil, err
- }
-
- stager.Current = "reading base vulnerabilities"
- baseVulns, err := s.GetAllVulnerabilities()
- rowsProgress.Increment()
- if err != nil {
- return nil, err
- }
-
- stager.Current = "preparing"
- baseVulnPkgMap := buildVulnerabilityPkgsMap(baseVulns)
- targetVulnPkgMap := buildVulnerabilityPkgsMap(targetVulns)
-
- stager.Current = "comparing vulnerabilities"
- allDiffsMap := diffVulnerabilities(baseVulns, targetVulns, baseVulnPkgMap, targetVulnPkgMap, diffItems)
-
- stager.Current = "reading base metadata"
- baseMetadata, err := s.GetAllVulnerabilityMetadata()
- if err != nil {
- return nil, err
- }
- rowsProgress.Increment()
-
- stager.Current = "reading target metadata"
- targetMetadata, err := targetStore.GetAllVulnerabilityMetadata()
- if err != nil {
- return nil, err
- }
- rowsProgress.Increment()
-
- stager.Current = "comparing metadata"
- metaDiffsMap := diffVulnerabilityMetadata(baseMetadata, targetMetadata, baseVulnPkgMap, targetVulnPkgMap, diffItems)
- for k, diff := range *metaDiffsMap {
- (*allDiffsMap)[k] = diff
- }
- allDiffs := []v3.Diff{}
- for _, diff := range *allDiffsMap {
- allDiffs = append(allDiffs, *diff)
- }
-
- rowsProgress.SetCompleted()
- diffItems.SetCompleted()
-
- return &allDiffs, nil
-}
-
-func (s *store) Close() {
- s.db.Exec("VACUUM;")
-}
diff --git a/grype/db/v3/store/store_test.go b/grype/db/v3/store/store_test.go
deleted file mode 100644
index 8dd1b4f5489..00000000000
--- a/grype/db/v3/store/store_test.go
+++ /dev/null
@@ -1,1174 +0,0 @@
-package store
-
-import (
- "encoding/json"
- "sort"
- "testing"
- "time"
-
- "github.com/go-test/deep"
- "github.com/stretchr/testify/assert"
-
- v3 "github.com/anchore/grype/grype/db/v3"
- "github.com/anchore/grype/grype/db/v3/store/model"
-)
-
-func assertIDReader(t *testing.T, reader v3.IDReader, expected v3.ID) {
- t.Helper()
- if actual, err := reader.GetID(); err != nil {
- t.Fatalf("failed to get ID: %+v", err)
- } else {
- diffs := deep.Equal(&expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
-}
-
-func TestStore_GetID_SetID(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- expected := v3.ID{
- BuildTimestamp: time.Now().UTC(),
- SchemaVersion: 2,
- }
-
- if err = s.SetID(expected); err != nil {
- t.Fatalf("failed to set ID: %+v", err)
- }
-
- assertIDReader(t, s, expected)
-
-}
-
-func assertVulnerabilityReader(t *testing.T, reader v3.VulnerabilityStoreReader, namespace, name string, expected []v3.Vulnerability) {
- if actual, err := reader.GetVulnerability(namespace, name); err != nil {
- t.Fatalf("failed to get Vulnerability: %+v", err)
- } else {
- if len(actual) != len(expected) {
- t.Fatalf("unexpected number of vulns: %d", len(actual))
- }
- for idx := range actual {
- diffs := deep.Equal(expected[idx], actual[idx])
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
- }
-}
-
-func TestStore_GetVulnerability_SetVulnerability(t *testing.T) {
- dbTempFile := t.TempDir()
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- extra := []v3.Vulnerability{
- {
- ID: "my-cve-33333",
- PackageName: "package-name-2",
- Namespace: "my-namespace",
- VersionConstraint: "< 1.0",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- RelatedVulnerabilities: []v3.VulnerabilityReference{
- {
- ID: "another-cve",
- Namespace: "nvd",
- },
- {
- ID: "an-other-cve",
- Namespace: "nvd",
- },
- },
- Fix: v3.Fix{
- Versions: []string{"2.0.1"},
- State: v3.FixedState,
- },
- },
- {
- ID: "my-other-cve-33333",
- PackageName: "package-name-3",
- Namespace: "my-namespace",
- VersionConstraint: "< 509.2.2",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- RelatedVulnerabilities: []v3.VulnerabilityReference{
- {
- ID: "another-cve",
- Namespace: "nvd",
- },
- {
- ID: "an-other-cve",
- Namespace: "nvd",
- },
- },
- Fix: v3.Fix{
- State: v3.NotFixedState,
- },
- },
- }
-
- expected := []v3.Vulnerability{
- {
- ID: "my-cve",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 1.0",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- RelatedVulnerabilities: []v3.VulnerabilityReference{
- {
- ID: "another-cve",
- Namespace: "nvd",
- },
- {
- ID: "an-other-cve",
- Namespace: "nvd",
- },
- },
- Fix: v3.Fix{
- Versions: []string{"1.0.1"},
- State: v3.FixedState,
- },
- },
- {
- ID: "my-other-cve",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 509.2.2",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- RelatedVulnerabilities: []v3.VulnerabilityReference{
- {
- ID: "another-cve",
- Namespace: "nvd",
- },
- {
- ID: "an-other-cve",
- Namespace: "nvd",
- },
- },
- Fix: v3.Fix{
- Versions: []string{"4.0.5"},
- State: v3.FixedState,
- },
- },
- }
-
- total := append(expected, extra...)
-
- if err = s.AddVulnerability(total...); err != nil {
- t.Fatalf("failed to set Vulnerability: %+v", err)
- }
-
- var allEntries []model.VulnerabilityModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != len(total) {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- assertVulnerabilityReader(t, s, expected[0].Namespace, expected[0].PackageName, expected)
-
-}
-
-func assertVulnerabilityMetadataReader(t *testing.T, reader v3.VulnerabilityMetadataStoreReader, id, namespace string, expected v3.VulnerabilityMetadata) {
- if actual, err := reader.GetVulnerabilityMetadata(id, namespace); err != nil {
- t.Fatalf("failed to get metadata: %+v", err)
- } else if actual == nil {
- t.Fatalf("no metadata returned for id=%q namespace=%q", id, namespace)
- } else {
- sortMetadataCvss(actual.Cvss)
- sortMetadataCvss(expected.Cvss)
-
- // make sure they both have the same number of CVSS entries - preventing a panic on later assertions
- assert.Len(t, expected.Cvss, len(actual.Cvss))
- for idx, actualCvss := range actual.Cvss {
- assert.Equal(t, actualCvss.Vector, expected.Cvss[idx].Vector)
- assert.Equal(t, actualCvss.Version, expected.Cvss[idx].Version)
- assert.Equal(t, actualCvss.Metrics, expected.Cvss[idx].Metrics)
-
- actualVendor, err := json.Marshal(actualCvss.VendorMetadata)
- if err != nil {
- t.Errorf("unable to marshal vendor metadata: %q", err)
- }
- expectedVendor, err := json.Marshal(expected.Cvss[idx].VendorMetadata)
- if err != nil {
- t.Errorf("unable to marshal vendor metadata: %q", err)
- }
- assert.Equal(t, string(actualVendor), string(expectedVendor))
-
- }
-
- // nil the Cvss field because it is an interface - verification of Cvss
- // has already happened at this point
- expected.Cvss = nil
- actual.Cvss = nil
- assert.Equal(t, &expected, actual)
- }
-
-}
-
-func sortMetadataCvss(cvss []v3.Cvss) {
- sort.Slice(cvss, func(i, j int) bool {
- // first, sort by Vector
- if cvss[i].Vector > cvss[j].Vector {
- return true
- }
- if cvss[i].Vector < cvss[j].Vector {
- return false
- }
- // then try to sort by BaseScore if Vector is the same
- return cvss[i].Metrics.BaseScore < cvss[j].Metrics.BaseScore
- })
-}
-
-// CustomMetadata is effectively a noop, its values aren't meaningful and are
-// mostly useful to ensure that any type can be stored and then retrieved for
-// assertion in these test cases where custom vendor CVSS scores are used
-type CustomMetadata struct {
- SuperScore string
- Vendor string
-}
-
-func TestStore_GetVulnerabilityMetadata_SetVulnerabilityMetadata(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- total := []v3.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v3.Cvss{
- {
- VendorMetadata: CustomMetadata{
- Vendor: "redhat",
- SuperScore: "1000",
- },
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 1.1,
- 2.2,
- 3.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NOT",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.3,
- 2.1,
- 3.2,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NICE",
- VendorMetadata: nil,
- },
- },
- },
- {
- ID: "my-other-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- }
-
- if err = s.AddVulnerabilityMetadata(total...); err != nil {
- t.Fatalf("failed to set metadata: %+v", err)
- }
-
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != len(total) {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
-}
-
-func TestStore_MergeVulnerabilityMetadata(t *testing.T) {
- tests := []struct {
- name string
- add []v3.VulnerabilityMetadata
- expected v3.VulnerabilityMetadata
- err bool
- }{
- {
- name: "go-case",
- add: []v3.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v3.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- {
- name: "merge-links",
- add: []v3.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://google.com"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://yahoo.com"},
- },
- },
- expected: v3.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re", "https://google.com", "https://yahoo.com"},
- Cvss: []v3.Cvss{},
- },
- },
- {
- name: "bad-severity",
- add: []v3.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "meh, push that for next tuesday...",
- URLs: []string{"https://redhat.com"},
- },
- },
- err: true,
- },
- {
- name: "mismatch-description",
- err: true,
- add: []v3.VulnerabilityMetadata{
- {
-
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- },
- {
- name: "mismatch-cvss2",
- err: false,
- add: []v3.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v3.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:P--VERY",
- },
- },
- },
- },
- {
- name: "mismatch-cvss3",
- err: false,
- add: []v3.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 0,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v3.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 0,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- dbTempDir := t.TempDir()
- s, err := New(dbTempDir, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- // add each metadata in order
- var theErr error
- for _, metadata := range test.add {
- err = s.AddVulnerabilityMetadata(metadata)
- if err != nil {
- theErr = err
- break
- }
- }
-
- if test.err && theErr == nil {
- t.Fatalf("expected error but did not get one")
- } else if !test.err && theErr != nil {
- t.Fatalf("expected no error but got one: %+v", theErr)
- } else if test.err && theErr != nil {
- // test pass...
- return
- }
-
- // ensure there is exactly one entry
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != 1 {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- // get the resulting metadata object
- if actual, err := s.GetVulnerabilityMetadata(test.expected.ID, test.expected.Namespace); err != nil {
- t.Fatalf("failed to get metadata: %+v", err)
- } else {
- diffs := deep.Equal(&test.expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
- })
- }
-}
-
-func TestCvssScoresInMetadata(t *testing.T) {
- tests := []struct {
- name string
- add []v3.VulnerabilityMetadata
- expected v3.VulnerabilityMetadata
- }{
- {
- name: "append-cvss",
- add: []v3.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v3.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- {
- name: "append-vendor-cvss",
- add: []v3.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- VendorMetadata: CustomMetadata{
- SuperScore: "100",
- Vendor: "debian",
- },
- },
- },
- },
- },
- expected: v3.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "2.0",
- Metrics: v3.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- VendorMetadata: CustomMetadata{
- SuperScore: "100",
- Vendor: "debian",
- },
- },
- },
- },
- },
- {
- name: "avoids-duplicate-cvss",
- add: []v3.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v3.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v3.Cvss{
- {
- Version: "3.0",
- Metrics: v3.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- dbTempDir := t.TempDir()
-
- s, err := New(dbTempDir, true)
- if err != nil {
- t.Fatalf("could not create s: %+v", err)
- }
-
- // add each metadata in order
- for _, metadata := range test.add {
- err = s.AddVulnerabilityMetadata(metadata)
- if err != nil {
- t.Fatalf("unable to s vulnerability metadata: %+v", err)
- }
- }
-
- // ensure there is exactly one entry
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != 1 {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- assertVulnerabilityMetadataReader(t, s, test.expected.ID, test.expected.Namespace, test.expected)
- })
- }
-}
-
-func Test_DiffStore(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
-
- s1, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
- dbTempFile = t.TempDir()
-
- s2, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- baseVulns := []v3.Vulnerability{
- {
- Namespace: "github:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 2.0 >= 1.29",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "github:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- PackageName: "npm:axios",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"},
- Fix: v3.Fix{
- State: v3.UnknownFixState,
- },
- },
- {
- Namespace: "nuget",
- ID: "GHSA-****-******",
- PackageName: "nuget:net",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:nuget:net:*:*:*:*:*:*"},
- Fix: v3.Fix{
- State: v3.UnknownFixState,
- },
- },
- {
- Namespace: "hex",
- ID: "GHSA-^^^^-^^^^^^",
- PackageName: "hex:esbuild",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:hex:esbuild:*:*:*:*:*:*"},
- },
- }
- baseMetadata := []v3.VulnerabilityMetadata{
- {
- Namespace: "nuget",
- ID: "GHSA-****-******",
- DataSource: "nvd",
- },
- }
- targetVulns := []v3.Vulnerability{
- {
- Namespace: "github:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 2.0 >= 1.29",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "github:go",
- ID: "GHSA-....-....",
- PackageName: "hashicorp:nomad",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:golang:hashicorp:nomad:*:*:*:*:*"},
- },
- {
- Namespace: "github:go",
- ID: "GHSA-....-....",
- PackageName: "hashicorp:n",
- VersionConstraint: "< 2.0 >= 1.17",
- CPEs: []string{"cpe:2.3:golang:hashicorp:n:*:*:*:*:*"},
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- PackageName: "npm:axios",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"},
- Fix: v3.Fix{
- State: v3.WontFixState,
- },
- },
- {
- Namespace: "nuget",
- ID: "GHSA-****-******",
- PackageName: "nuget:net",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:nuget:net:*:*:*:*:*:*"},
- Fix: v3.Fix{
- State: v3.UnknownFixState,
- },
- },
- }
- expectedDiffs := []v3.Diff{
- {
- Reason: v3.DiffChanged,
- ID: "CVE-123-4567",
- Namespace: "github:python",
- Packages: []string{"pypi:requests"},
- },
- {
- Reason: v3.DiffChanged,
- ID: "CVE-123-7654",
- Namespace: "npm",
- Packages: []string{"npm:axios"},
- },
- {
- Reason: v3.DiffRemoved,
- ID: "GHSA-****-******",
- Namespace: "nuget",
- Packages: []string{"nuget:net"},
- },
- {
- Reason: v3.DiffAdded,
- ID: "GHSA-....-....",
- Namespace: "github:go",
- Packages: []string{"hashicorp:n", "hashicorp:nomad"},
- },
- {
- Reason: v3.DiffRemoved,
- ID: "GHSA-^^^^-^^^^^^",
- Namespace: "hex",
- Packages: []string{"hex:esbuild"},
- },
- }
-
- for _, vuln := range baseVulns {
- s1.AddVulnerability(vuln)
- }
- for _, vuln := range targetVulns {
- s2.AddVulnerability(vuln)
- }
- for _, meta := range baseMetadata {
- s1.AddVulnerabilityMetadata(meta)
- }
-
- //WHEN
- result, err := s1.DiffStore(s2)
-
- //THEN
- sort.SliceStable(*result, func(i, j int) bool {
- return (*result)[i].ID < (*result)[j].ID
- })
- for i := range *result {
- sort.Strings((*result)[i].Packages)
- }
-
- assert.NoError(t, err)
- assert.Equal(t, expectedDiffs, *result)
-}
diff --git a/grype/db/v3/vulnerability.go b/grype/db/v3/vulnerability.go
deleted file mode 100644
index 19f9e4a1697..00000000000
--- a/grype/db/v3/vulnerability.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package v3
-
-import (
- "sort"
- "strings"
-)
-
-// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.
-type Vulnerability struct {
- ID string // The identifier of the vulnerability or advisory
- PackageName string // The name of the package that is vulnerable
- Namespace string // The ecosystem where the package resides
- VersionConstraint string // The version range which the given package is vulnerable
- VersionFormat string // The format which all version fields should be interpreted as
- CPEs []string // The CPEs which are considered vulnerable
- RelatedVulnerabilities []VulnerabilityReference // Other Vulnerabilities that are related to this one (e.g. GHSA relate to CVEs, or how distro CVE relates to NVD record)
- Fix Fix // All information about fixed versions
- Advisories []Advisory // Any vendor advisories about fixes or other notifications about this vulnerability
-}
-
-type VulnerabilityReference struct {
- ID string
- Namespace string
-}
-
-//nolint:gocognit
-func (v *Vulnerability) Equal(vv Vulnerability) bool {
- equal := v.ID == vv.ID &&
- v.PackageName == vv.PackageName &&
- v.Namespace == vv.Namespace &&
- v.VersionConstraint == vv.VersionConstraint &&
- v.VersionFormat == vv.VersionFormat &&
- len(v.CPEs) == len(vv.CPEs) &&
- len(v.RelatedVulnerabilities) == len(vv.RelatedVulnerabilities) &&
- len(v.Advisories) == len(vv.Advisories) &&
- v.Fix.State == vv.Fix.State &&
- len(v.Fix.Versions) == len(vv.Fix.Versions)
-
- if !equal {
- return false
- }
-
- sort.Strings(v.CPEs)
- sort.Strings(vv.CPEs)
- for idx, cpe := range v.CPEs {
- if cpe != vv.CPEs[idx] {
- return false
- }
- }
-
- sortedBaseRelVulns, sortedTargetRelVulns := sortRelatedVulns(v.RelatedVulnerabilities), sortRelatedVulns(vv.RelatedVulnerabilities)
- for idx, item := range sortedBaseRelVulns {
- if item != sortedTargetRelVulns[idx] {
- return false
- }
- }
- sortedBaseAdvisories, sortedTargetAdvisories := sortAdvisories(v.Advisories), sortAdvisories(vv.Advisories)
- for idx, item := range sortedBaseAdvisories {
- if item != sortedTargetAdvisories[idx] {
- return false
- }
- }
- sort.Strings(v.Fix.Versions)
- sort.Strings(vv.Fix.Versions)
- for idx, item := range v.Fix.Versions {
- if item != vv.Fix.Versions[idx] {
- return false
- }
- }
-
- return true
-}
-
-func sortRelatedVulns(vulns []VulnerabilityReference) []VulnerabilityReference {
- sort.SliceStable(vulns, func(i, j int) bool {
- b1, b2 := strings.Builder{}, strings.Builder{}
- b1.WriteString(vulns[i].ID)
- b1.WriteString(vulns[i].Namespace)
- b2.WriteString(vulns[j].ID)
- b2.WriteString(vulns[j].Namespace)
- return b1.String() < b2.String()
- })
- return vulns
-}
-
-func sortAdvisories(advisories []Advisory) []Advisory {
- sort.SliceStable(advisories, func(i, j int) bool {
- b1, b2 := strings.Builder{}, strings.Builder{}
- b1.WriteString(advisories[i].ID)
- b1.WriteString(advisories[i].Link)
- b2.WriteString(advisories[j].ID)
- b2.WriteString(advisories[j].Link)
- return b1.String() < b2.String()
- })
- return advisories
-}
diff --git a/grype/db/v3/vulnerability_metadata.go b/grype/db/v3/vulnerability_metadata.go
deleted file mode 100644
index b30f879ca11..00000000000
--- a/grype/db/v3/vulnerability_metadata.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package v3
-
-import "reflect"
-
-// VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching.
-type VulnerabilityMetadata struct {
- ID string // The identifier of the vulnerability or advisory
- Namespace string // Where this entry is valid within
- DataSource string // A URL where the data was sourced from
- RecordSource string // The source of the vulnerability information (relative to the immediate upstream in the enterprise feedgroup)
- Severity string // How severe the vulnerability is (valid values are defined by upstream sources currently)
- URLs []string // URLs to get more information about the vulnerability or advisory
- Description string // Description of the vulnerability
- Cvss []Cvss // Common Vulnerability Scoring System values
-}
-
-// Cvss contains select Common Vulnerability Scoring System fields for a vulnerability.
-type Cvss struct {
- // VendorMetadata captures non-standard CVSS fields that vendors can sometimes
- // include when providing CVSS information. This vendor-specific metadata type
- // allows to capture that data for persisting into the database
- VendorMetadata interface{}
- Metrics CvssMetrics
- Vector string // A textual representation of the metric values used to determine the score
- Version string // The version of the CVSS spec, for example 2.0, 3.0, or 3.1
-}
-
-// CvssMetrics are the quantitative values that make up a CVSS score.
-type CvssMetrics struct {
- // BaseScore ranges from 0 - 10 and defines qualities intrinsic to the severity of a vulnerability.
- BaseScore float64
- // ExploitabilityScore is a pointer to avoid having a 0 value by default.
- // It is an indicator of how easy it may be for an attacker to exploit
- // a vulnerability
- ExploitabilityScore *float64
- // ImpactScore represents the effects of an exploited vulnerability
- // relative to compromise in confidentiality, integrity, and availability.
- // It is an optional parameter, so that is why it is a pointer instead of
- // a regular field
- ImpactScore *float64
-}
-
-func NewCvssMetrics(baseScore, exploitabilityScore, impactScore float64) CvssMetrics {
- return CvssMetrics{
- BaseScore: baseScore,
- ExploitabilityScore: &exploitabilityScore,
- ImpactScore: &impactScore,
- }
-}
-
-func (v *VulnerabilityMetadata) Equal(vv VulnerabilityMetadata) bool {
- equal := v.ID == vv.ID &&
- v.Namespace == vv.Namespace &&
- v.DataSource == vv.DataSource &&
- v.RecordSource == vv.RecordSource &&
- v.Severity == vv.Severity &&
- v.Description == vv.Description &&
- len(v.URLs) == len(vv.URLs) &&
- len(v.Cvss) == len(vv.Cvss)
-
- if !equal {
- return false
- }
- for idx, cpe := range v.URLs {
- if cpe != vv.URLs[idx] {
- return false
- }
- }
- for idx, item := range v.Cvss {
- if !reflect.DeepEqual(item, vv.Cvss[idx]) {
- return false
- }
- }
-
- return true
-}
diff --git a/grype/db/v3/vulnerability_metadata_store.go b/grype/db/v3/vulnerability_metadata_store.go
deleted file mode 100644
index 83ba195f544..00000000000
--- a/grype/db/v3/vulnerability_metadata_store.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package v3
-
-type SerializedVulnerabilityMetadata = interface{}
-
-type VulnerabilityMetadataStore interface {
- VulnerabilityMetadataStoreReader
- VulnerabilityMetadataStoreWriter
-}
-
-type VulnerabilityMetadataStoreReader interface {
- GetVulnerabilityMetadata(id, namespace string) (*VulnerabilityMetadata, error)
- GetAllVulnerabilityMetadata() (*[]VulnerabilityMetadata, error)
-}
-
-type VulnerabilityMetadataStoreWriter interface {
- AddVulnerabilityMetadata(metadata ...VulnerabilityMetadata) error
-}
diff --git a/grype/db/v3/vulnerability_store.go b/grype/db/v3/vulnerability_store.go
deleted file mode 100644
index 6abdd5dab35..00000000000
--- a/grype/db/v3/vulnerability_store.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package v3
-
-const VulnerabilityStoreFileName = "vulnerability.db"
-
-type SerializedVulnerabilities = interface{}
-
-type VulnerabilityStore interface {
- VulnerabilityStoreReader
- VulnerabilityStoreWriter
-}
-
-type VulnerabilityStoreReader interface {
- // GetVulnerability retrieves vulnerabilities associated with a namespace and a package name
- GetVulnerability(namespace, name string) ([]Vulnerability, error)
- GetAllVulnerabilities() (*[]Vulnerability, error)
-}
-
-type VulnerabilityStoreWriter interface {
- // AddVulnerability inserts a new record of a vulnerability into the store
- AddVulnerability(vulnerabilities ...Vulnerability) error
-}
diff --git a/grype/db/v4/advisory.go b/grype/db/v4/advisory.go
deleted file mode 100644
index 012c04dd038..00000000000
--- a/grype/db/v4/advisory.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package v4
-
-// Advisory represents published statements regarding a vulnerability (and potentially about it's resolution).
-type Advisory struct {
- ID string `json:"id"`
- Link string `json:"link"`
-}
diff --git a/grype/db/v4/diff.go b/grype/db/v4/diff.go
deleted file mode 100644
index 86df8738d82..00000000000
--- a/grype/db/v4/diff.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package v4
-
-type DiffReason = string
-
-const (
- DiffAdded DiffReason = "added"
- DiffChanged DiffReason = "changed"
- DiffRemoved DiffReason = "removed"
-)
-
-type Diff struct {
- Reason DiffReason `json:"reason"`
- ID string `json:"id"`
- Namespace string `json:"namespace"`
- Packages []string `json:"packages"`
-}
diff --git a/grype/db/v4/fix.go b/grype/db/v4/fix.go
deleted file mode 100644
index e8dba213c8a..00000000000
--- a/grype/db/v4/fix.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package v4
-
-type FixState string
-
-const (
- UnknownFixState FixState = "unknown"
- FixedState FixState = "fixed"
- NotFixedState FixState = "not-fixed"
- WontFixState FixState = "wont-fix"
-)
-
-// Fix represents all information about known fixes for a stated vulnerability.
-type Fix struct {
- Versions []string `json:"versions"` // The version(s) which this particular vulnerability was fixed in
- State FixState `json:"state"`
-}
diff --git a/grype/db/v4/id.go b/grype/db/v4/id.go
deleted file mode 100644
index 369c4c66b8f..00000000000
--- a/grype/db/v4/id.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package v4
-
-import (
- "time"
-)
-
-// ID represents identifying information for a DB and the data it contains.
-type ID struct {
- // BuildTimestamp is the timestamp used to define the age of the DB, ideally including the age of the data
- // contained in the DB, not just when the DB file was created.
- BuildTimestamp time.Time `json:"build_timestamp"`
- SchemaVersion int `json:"schema_version"`
-}
-
-type IDReader interface {
- GetID() (*ID, error)
-}
-
-type IDWriter interface {
- SetID(ID) error
-}
-
-func NewID(age time.Time) ID {
- return ID{
- BuildTimestamp: age.UTC(),
- SchemaVersion: SchemaVersion,
- }
-}
diff --git a/grype/db/v4/namespace/cpe/namespace.go b/grype/db/v4/namespace/cpe/namespace.go
deleted file mode 100644
index 0e595f0bb1e..00000000000
--- a/grype/db/v4/namespace/cpe/namespace.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package cpe
-
-import (
- "errors"
- "fmt"
- "strings"
-
- "github.com/anchore/grype/grype/db/v4/pkg/resolver"
- "github.com/anchore/grype/grype/db/v4/pkg/resolver/stock"
-)
-
-const ID = "cpe"
-
-type Namespace struct {
- provider string
- resolver resolver.Resolver
-}
-
-func NewNamespace(provider string) *Namespace {
- return &Namespace{
- provider: provider,
- resolver: &stock.Resolver{},
- }
-}
-
-func FromString(namespaceStr string) (*Namespace, error) {
- if namespaceStr == "" {
- return nil, errors.New("unable to create CPE namespace from empty string")
- }
-
- components := strings.Split(namespaceStr, ":")
-
- if len(components) != 2 {
- return nil, fmt.Errorf("unable to create CPE namespace from %s: incorrect number of components", namespaceStr)
- }
-
- if components[1] != ID {
- return nil, fmt.Errorf("unable to create CPE namespace from %s: type %s is incorrect", namespaceStr, components[1])
- }
-
- return NewNamespace(components[0]), nil
-}
-
-func (n *Namespace) Provider() string {
- return n.provider
-}
-
-func (n *Namespace) Resolver() resolver.Resolver {
- return n.resolver
-}
-
-func (n Namespace) String() string {
- return fmt.Sprintf("%s:%s", n.provider, ID)
-}
diff --git a/grype/db/v4/namespace/cpe/namespace_test.go b/grype/db/v4/namespace/cpe/namespace_test.go
deleted file mode 100644
index e4be6dc11a0..00000000000
--- a/grype/db/v4/namespace/cpe/namespace_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package cpe
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestFromString(t *testing.T) {
- successTests := []struct {
- namespaceString string
- result *Namespace
- }{
- {
- namespaceString: "abc.xyz:cpe",
- result: NewNamespace("abc.xyz"),
- },
- }
-
- for _, test := range successTests {
- result, _ := FromString(test.namespaceString)
- assert.Equal(t, result, test.result)
- }
-
- errorTests := []struct {
- namespaceString string
- errorMessage string
- }{
- {
- namespaceString: "",
- errorMessage: "unable to create CPE namespace from empty string",
- },
- {
- namespaceString: "single-component",
- errorMessage: "unable to create CPE namespace from single-component: incorrect number of components",
- },
- {
- namespaceString: "too:many:components",
- errorMessage: "unable to create CPE namespace from too:many:components: incorrect number of components",
- },
- {
- namespaceString: "wrong:namespace_type",
- errorMessage: "unable to create CPE namespace from wrong:namespace_type: type namespace_type is incorrect",
- },
- }
-
- for _, test := range errorTests {
- _, err := FromString(test.namespaceString)
- assert.EqualError(t, err, test.errorMessage)
- }
-}
diff --git a/grype/db/v4/namespace/distro/namespace.go b/grype/db/v4/namespace/distro/namespace.go
deleted file mode 100644
index b9a82bfb12f..00000000000
--- a/grype/db/v4/namespace/distro/namespace.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package distro
-
-import (
- "errors"
- "fmt"
- "strings"
-
- "github.com/anchore/grype/grype/db/v4/pkg/resolver"
- "github.com/anchore/grype/grype/db/v4/pkg/resolver/stock"
- "github.com/anchore/grype/grype/distro"
-)
-
-const ID = "distro"
-
-type Namespace struct {
- provider string
- distroType distro.Type
- version string
- resolver resolver.Resolver
-}
-
-func NewNamespace(provider string, distroType distro.Type, version string) *Namespace {
- return &Namespace{
- provider: provider,
- distroType: distroType,
- version: version,
- resolver: &stock.Resolver{},
- }
-}
-
-func FromString(namespaceStr string) (*Namespace, error) {
- if namespaceStr == "" {
- return nil, errors.New("unable to create distro namespace from empty string")
- }
-
- components := strings.Split(namespaceStr, ":")
-
- if len(components) != 4 {
- return nil, fmt.Errorf("unable to create distro namespace from %s: incorrect number of components", namespaceStr)
- }
-
- if components[1] != ID {
- return nil, fmt.Errorf("unable to create distro namespace from %s: type %s is incorrect", namespaceStr, components[1])
- }
-
- return NewNamespace(components[0], distro.Type(components[2]), components[3]), nil
-}
-
-func (n *Namespace) Provider() string {
- return n.provider
-}
-
-func (n *Namespace) DistroType() distro.Type {
- return n.distroType
-}
-
-func (n *Namespace) Version() string {
- return n.version
-}
-
-func (n *Namespace) Resolver() resolver.Resolver {
- return n.resolver
-}
-
-func (n Namespace) String() string {
- return fmt.Sprintf("%s:%s:%s:%s", n.provider, ID, n.distroType, n.version)
-}
diff --git a/grype/db/v4/namespace/distro/namespace_test.go b/grype/db/v4/namespace/distro/namespace_test.go
deleted file mode 100644
index f916d66b6d3..00000000000
--- a/grype/db/v4/namespace/distro/namespace_test.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package distro
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- grypeDistro "github.com/anchore/grype/grype/distro"
-)
-
-func TestFromString(t *testing.T) {
- successTests := []struct {
- namespaceString string
- result *Namespace
- }{
- {
- namespaceString: "alpine:distro:alpine:3.15",
- result: NewNamespace("alpine", grypeDistro.Alpine, "3.15"),
- },
- {
- namespaceString: "redhat:distro:redhat:8",
- result: NewNamespace("redhat", grypeDistro.RedHat, "8"),
- },
- {
- namespaceString: "abc.xyz:distro:unknown:abcd~~~",
- result: NewNamespace("abc.xyz", grypeDistro.Type("unknown"), "abcd~~~"),
- },
- {
- namespaceString: "msrc:distro:windows:10111",
- result: NewNamespace("msrc", grypeDistro.Type("windows"), "10111"),
- },
- {
- namespaceString: "amazon:distro:amazonlinux:2022",
- result: NewNamespace("amazon", grypeDistro.AmazonLinux, "2022"),
- },
- {
- namespaceString: "amazon:distro:amazonlinux:2",
- result: NewNamespace("amazon", grypeDistro.AmazonLinux, "2"),
- },
- {
- namespaceString: "wolfi:distro:wolfi:rolling",
- result: NewNamespace("wolfi", grypeDistro.Wolfi, "rolling"),
- },
- }
-
- for _, test := range successTests {
- result, _ := FromString(test.namespaceString)
- assert.Equal(t, result, test.result)
- }
-
- errorTests := []struct {
- namespaceString string
- errorMessage string
- }{
- {
- namespaceString: "",
- errorMessage: "unable to create distro namespace from empty string",
- },
- {
- namespaceString: "single-component",
- errorMessage: "unable to create distro namespace from single-component: incorrect number of components",
- },
- {
- namespaceString: "two:components",
- errorMessage: "unable to create distro namespace from two:components: incorrect number of components",
- },
- {
- namespaceString: "still:not:enough",
- errorMessage: "unable to create distro namespace from still:not:enough: incorrect number of components",
- },
- {
- namespaceString: "too:many:components:a:b",
- errorMessage: "unable to create distro namespace from too:many:components:a:b: incorrect number of components",
- },
- {
- namespaceString: "wrong:namespace_type:a:b",
- errorMessage: "unable to create distro namespace from wrong:namespace_type:a:b: type namespace_type is incorrect",
- },
- }
-
- for _, test := range errorTests {
- _, err := FromString(test.namespaceString)
- assert.EqualError(t, err, test.errorMessage)
- }
-}
diff --git a/grype/db/v4/namespace/from_string.go b/grype/db/v4/namespace/from_string.go
deleted file mode 100644
index 0dcd509fe3d..00000000000
--- a/grype/db/v4/namespace/from_string.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package namespace
-
-import (
- "errors"
- "fmt"
- "strings"
-
- "github.com/anchore/grype/grype/db/v4/namespace/cpe"
- "github.com/anchore/grype/grype/db/v4/namespace/distro"
- "github.com/anchore/grype/grype/db/v4/namespace/language"
-)
-
-func FromString(namespaceStr string) (Namespace, error) {
- if namespaceStr == "" {
- return nil, errors.New("unable to create namespace from empty string")
- }
-
- components := strings.Split(namespaceStr, ":")
-
- if len(components) < 1 {
- return nil, fmt.Errorf("unable to create namespace from %s: incorrect number of components", namespaceStr)
- }
-
- switch components[1] {
- case cpe.ID:
- return cpe.FromString(namespaceStr)
- case distro.ID:
- return distro.FromString(namespaceStr)
- case language.ID:
- return language.FromString(namespaceStr)
- default:
- return nil, fmt.Errorf("unable to create namespace from %s: unknown type %s", namespaceStr, components[1])
- }
-}
diff --git a/grype/db/v4/namespace/from_string_test.go b/grype/db/v4/namespace/from_string_test.go
deleted file mode 100644
index e20b4f6a969..00000000000
--- a/grype/db/v4/namespace/from_string_test.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package namespace
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- "github.com/anchore/grype/grype/db/v4/namespace/cpe"
- "github.com/anchore/grype/grype/db/v4/namespace/distro"
- "github.com/anchore/grype/grype/db/v4/namespace/language"
- grypeDistro "github.com/anchore/grype/grype/distro"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-func TestFromString(t *testing.T) {
- tests := []struct {
- namespaceString string
- result Namespace
- }{
- {
- namespaceString: "github:language:python",
- result: language.NewNamespace("github", syftPkg.Python, ""),
- },
- {
- namespaceString: "github:language:python:python",
- result: language.NewNamespace("github", syftPkg.Python, syftPkg.PythonPkg),
- },
- {
- namespaceString: "debian:distro:debian:8",
- result: distro.NewNamespace("debian", grypeDistro.Debian, "8"),
- },
- {
- namespaceString: "unknown:distro:amazonlinux:2022.15",
- result: distro.NewNamespace("unknown", grypeDistro.AmazonLinux, "2022.15"),
- },
- {
- namespaceString: "ns-1:distro:unknowndistro:abcdefg~~~",
- result: distro.NewNamespace("ns-1", grypeDistro.Type("unknowndistro"), "abcdefg~~~"),
- },
- {
- namespaceString: "abc.xyz:cpe",
- result: cpe.NewNamespace("abc.xyz"),
- },
- }
-
- for _, test := range tests {
- result, _ := FromString(test.namespaceString)
- assert.Equal(t, result, test.result)
- }
-}
diff --git a/grype/db/v4/namespace/index.go b/grype/db/v4/namespace/index.go
deleted file mode 100644
index d6a902f8698..00000000000
--- a/grype/db/v4/namespace/index.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package namespace
-
-import (
- "fmt"
- "strings"
-
- "github.com/anchore/grype/grype/db/v4/namespace/cpe"
- "github.com/anchore/grype/grype/db/v4/namespace/distro"
- "github.com/anchore/grype/grype/db/v4/namespace/language"
- grypeDistro "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/internal/log"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-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
-}
-
-func (i *Index) NamespacesForDistro(d *grypeDistro.Distro) []*distro.Namespace {
- if d == nil {
- return nil
- }
-
- if d.IsRolling() {
- distroKey := fmt.Sprintf("%s:%s", strings.ToLower(d.Type.String()), "rolling")
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
- }
-
- var versionSegments []int
- if d.Version != nil {
- versionSegments = d.Version.Segments()
- }
-
- if len(versionSegments) > 0 {
- // First attempt a direct match on distro full name and version
- distroKey := fmt.Sprintf("%s:%s", strings.ToLower(d.Type.String()), d.FullVersion())
-
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
-
- if len(versionSegments) == 3 {
- // Try with only first two version components
- distroKey = fmt.Sprintf("%s:%d.%d", strings.ToLower(d.Type.String()), versionSegments[0], versionSegments[1])
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
-
- // Try using only major version component
- distroKey = fmt.Sprintf("%s:%d", strings.ToLower(d.Type.String()), versionSegments[0])
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
- }
-
- // Fall back into the manual mapping logic derived from
- // https://github.com/anchore/enterprise/blob/eb71bc6686b9f4c92347a4e95bec828cee879197/anchore_engine/services/policy_engine/__init__.py#L127-L140
- switch d.Type {
- case grypeDistro.CentOS, grypeDistro.RedHat, grypeDistro.Fedora, grypeDistro.RockyLinux, grypeDistro.AlmaLinux, grypeDistro.Gentoo:
- // TODO: there is no mapping of fedora version to RHEL latest version (only the name)
- distroKey = fmt.Sprintf("%s:%d", strings.ToLower(string(grypeDistro.RedHat)), versionSegments[0])
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
- }
- }
-
- return nil
-}
-
-func (i *Index) CPENamespaces() []*cpe.Namespace {
- return i.cpe
-}
diff --git a/grype/db/v4/namespace/index_test.go b/grype/db/v4/namespace/index_test.go
deleted file mode 100644
index eab6b82f531..00000000000
--- a/grype/db/v4/namespace/index_test.go
+++ /dev/null
@@ -1,283 +0,0 @@
-package namespace
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- "github.com/anchore/grype/grype/db/v4/namespace/cpe"
- "github.com/anchore/grype/grype/db/v4/namespace/distro"
- "github.com/anchore/grype/grype/db/v4/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",
- "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.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 {
- distro, err := osDistro.New(dt, v, idLikes...)
- assert.NoError(t, err)
- return distro
-}
-
-func TestIndex_NamespacesForDistro(t *testing.T) {
- namespaceIndex, err := FromStrings([]string{
- "alpine:distro:alpine:3.15",
- "alpine:distro:alpine:3.16",
- "debian:distro:debian:8",
- "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",
- "msrc:distro:windows:471816",
- "ubuntu:distro:ubuntu:18.04",
- "oracle:distro:oraclelinux:8",
- "wolfi:distro:wolfi:rolling",
- "chainguard:distro:chainguard:rolling",
- "archlinux:distro:archlinux:rolling",
- })
-
- assert.NoError(t, err)
-
- tests := []struct {
- distro *osDistro.Distro
- namespaces []*distro.Namespace
- }{
- {
- distro: newDistro(t, osDistro.Alpine, "3.15.4", []string{"alpine"}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("alpine", osDistro.Alpine, "3.15"),
- },
- },
- {
- distro: newDistro(t, osDistro.Alpine, "3.16", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("alpine", osDistro.Alpine, "3.16"),
- },
- },
- {
- distro: newDistro(t, osDistro.Alpine, "3.16.4.5", []string{}),
- namespaces: nil,
- },
- {
- distro: newDistro(t, osDistro.Debian, "8.5", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("debian", osDistro.Debian, "8"),
- distro.NewNamespace("other-provider", osDistro.Debian, "8"),
- },
- },
- {
- distro: newDistro(t, osDistro.RedHat, "9.5", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("redhat", osDistro.RedHat, "9"),
- distro.NewNamespace("other-provider", osDistro.RedHat, "9"),
- },
- },
- {
- distro: newDistro(t, osDistro.CentOS, "9.5", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("redhat", osDistro.RedHat, "9"),
- distro.NewNamespace("other-provider", osDistro.RedHat, "9"),
- },
- },
- {
- distro: newDistro(t, osDistro.AlmaLinux, "9.5", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("redhat", osDistro.RedHat, "9"),
- distro.NewNamespace("other-provider", osDistro.RedHat, "9"),
- },
- },
- {
- distro: newDistro(t, osDistro.RockyLinux, "9.5", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("redhat", osDistro.RedHat, "9"),
- distro.NewNamespace("other-provider", osDistro.RedHat, "9"),
- },
- },
- {
- distro: newDistro(t, osDistro.SLES, "12.5", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("suse", osDistro.SLES, "12.5"),
- },
- },
- {
- distro: newDistro(t, osDistro.Windows, "471816", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("msrc", osDistro.Windows, "471816"),
- },
- },
- {
- distro: newDistro(t, osDistro.Ubuntu, "18.04", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("ubuntu", osDistro.Ubuntu, "18.04"),
- },
- },
- {
- distro: newDistro(t, osDistro.Fedora, "31.4", []string{}),
- namespaces: nil,
- },
- {
- distro: newDistro(t, osDistro.AmazonLinux, "2", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("amazon", osDistro.AmazonLinux, "2"),
- },
- },
- {
- distro: newDistro(t, osDistro.AmazonLinux, "2022", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("amazon", osDistro.AmazonLinux, "2022"),
- },
- },
- {
- distro: newDistro(t, osDistro.Mariner, "20.1", []string{}),
- namespaces: nil,
- },
- {
- distro: newDistro(t, osDistro.OracleLinux, "8", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("oracle", osDistro.OracleLinux, "8"),
- },
- },
- {
- distro: newDistro(t, osDistro.ArchLinux, "", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("archlinux", osDistro.ArchLinux, "rolling"),
- },
- },
- {
- // Gentoo is a rolling distro; however, because we currently have no namespaces populated for it in the
- // index fixture, we expect to get nil
- distro: newDistro(t, osDistro.Gentoo, "", []string{}),
- namespaces: nil,
- },
- {
- distro: newDistro(t, osDistro.OpenSuseLeap, "100", []string{}),
- namespaces: nil,
- },
- {
- distro: newDistro(t, osDistro.Photon, "20.1", []string{}),
- namespaces: nil,
- },
- {
- distro: newDistro(t, osDistro.Busybox, "20.1", []string{}),
- namespaces: nil,
- },
- {
- distro: newDistro(t, osDistro.Wolfi, "20221011", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("wolfi", osDistro.Wolfi, "rolling"),
- },
- },
- {
- distro: newDistro(t, osDistro.Chainguard, "20230214", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("chainguard", osDistro.Chainguard, "rolling"),
- },
- },
- }
-
- for _, test := range tests {
- result := namespaceIndex.NamespacesForDistro(test.distro)
- assert.ElementsMatch(t, result, test.namespaces)
- }
-}
diff --git a/grype/db/v4/namespace/language/namespace.go b/grype/db/v4/namespace/language/namespace.go
deleted file mode 100644
index 046ae04b51e..00000000000
--- a/grype/db/v4/namespace/language/namespace.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package language
-
-import (
- "errors"
- "fmt"
- "strings"
-
- "github.com/anchore/grype/grype/db/v4/pkg/resolver"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-const ID = "language"
-
-type Namespace struct {
- provider string
- language syftPkg.Language
- packageType syftPkg.Type
- resolver resolver.Resolver
-}
-
-func NewNamespace(provider string, language syftPkg.Language, packageType syftPkg.Type) *Namespace {
- r, _ := resolver.FromLanguage(language)
-
- return &Namespace{
- provider: provider,
- language: language,
- packageType: packageType,
- resolver: r,
- }
-}
-
-func FromString(namespaceStr string) (*Namespace, error) {
- if namespaceStr == "" {
- return nil, errors.New("unable to create language namespace from empty string")
- }
-
- components := strings.Split(namespaceStr, ":")
-
- if len(components) != 3 && len(components) != 4 {
- return nil, fmt.Errorf("unable to create language namespace from %s: incorrect number of components", namespaceStr)
- }
-
- if components[1] != ID {
- return nil, fmt.Errorf("unable to create language namespace from %s: type %s is incorrect", namespaceStr, components[1])
- }
-
- packageType := ""
-
- if len(components) == 4 {
- packageType = components[3]
- }
-
- return NewNamespace(components[0], syftPkg.Language(components[2]), syftPkg.Type(packageType)), nil
-}
-
-func (n *Namespace) Provider() string {
- return n.provider
-}
-
-func (n *Namespace) Language() syftPkg.Language {
- return n.language
-}
-
-func (n *Namespace) PackageType() syftPkg.Type {
- return n.packageType
-}
-
-func (n *Namespace) Resolver() resolver.Resolver {
- return n.resolver
-}
-
-func (n Namespace) String() string {
- if n.packageType != "" {
- return fmt.Sprintf("%s:%s:%s:%s", n.provider, ID, n.language, n.packageType)
- }
-
- return fmt.Sprintf("%s:%s:%s", n.provider, ID, n.language)
-}
diff --git a/grype/db/v4/namespace/language/namespace_test.go b/grype/db/v4/namespace/language/namespace_test.go
deleted file mode 100644
index 35cd74241b7..00000000000
--- a/grype/db/v4/namespace/language/namespace_test.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package language
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-func TestFromString(t *testing.T) {
- successTests := []struct {
- namespaceString string
- result *Namespace
- }{
- {
- namespaceString: "github:language:python",
- result: NewNamespace("github", syftPkg.Python, ""),
- },
- {
- namespaceString: "github:language:ruby",
- result: NewNamespace("github", syftPkg.Ruby, ""),
- },
- {
- namespaceString: "github:language:java",
- result: NewNamespace("github", syftPkg.Java, ""),
- },
- {
- namespaceString: "abc.xyz:language:something",
- result: NewNamespace("abc.xyz", syftPkg.Language("something"), ""),
- },
- {
- namespaceString: "abc.xyz:language:something:another-package-manager",
- result: NewNamespace("abc.xyz", syftPkg.Language("something"), syftPkg.Type("another-package-manager")),
- },
- }
-
- for _, test := range successTests {
- result, _ := FromString(test.namespaceString)
- assert.Equal(t, result, test.result)
- }
-
- errorTests := []struct {
- namespaceString string
- errorMessage string
- }{
- {
- namespaceString: "",
- errorMessage: "unable to create language namespace from empty string",
- },
- {
- namespaceString: "single-component",
- errorMessage: "unable to create language namespace from single-component: incorrect number of components",
- },
- {
- namespaceString: "two:components",
- errorMessage: "unable to create language namespace from two:components: incorrect number of components",
- },
- {
- namespaceString: "too:many:components:a:b",
- errorMessage: "unable to create language namespace from too:many:components:a:b: incorrect number of components",
- },
- {
- namespaceString: "wrong:namespace_type:a:b",
- errorMessage: "unable to create language namespace from wrong:namespace_type:a:b: type namespace_type is incorrect",
- },
- }
-
- for _, test := range errorTests {
- _, err := FromString(test.namespaceString)
- assert.EqualError(t, err, test.errorMessage)
- }
-}
diff --git a/grype/db/v4/namespace/namespace.go b/grype/db/v4/namespace/namespace.go
deleted file mode 100644
index 426fb8a8db8..00000000000
--- a/grype/db/v4/namespace/namespace.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package namespace
-
-import (
- "github.com/anchore/grype/grype/db/v4/pkg/resolver"
-)
-
-type Namespace interface {
- Provider() string
- Resolver() resolver.Resolver
- String() string
-}
diff --git a/grype/db/v4/pkg/resolver/from_language.go b/grype/db/v4/pkg/resolver/from_language.go
deleted file mode 100644
index 5393af21782..00000000000
--- a/grype/db/v4/pkg/resolver/from_language.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package resolver
-
-import (
- "github.com/anchore/grype/grype/db/v4/pkg/resolver/java"
- "github.com/anchore/grype/grype/db/v4/pkg/resolver/python"
- "github.com/anchore/grype/grype/db/v4/pkg/resolver/stock"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-func FromLanguage(language syftPkg.Language) (Resolver, error) {
- var r Resolver
-
- switch language {
- case syftPkg.Python:
- r = &python.Resolver{}
- case syftPkg.Java:
- r = &java.Resolver{}
- default:
- r = &stock.Resolver{}
- }
-
- return r, nil
-}
diff --git a/grype/db/v4/pkg/resolver/from_language_test.go b/grype/db/v4/pkg/resolver/from_language_test.go
deleted file mode 100644
index 1251bb522d4..00000000000
--- a/grype/db/v4/pkg/resolver/from_language_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package resolver
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- "github.com/anchore/grype/grype/db/v4/pkg/resolver/java"
- "github.com/anchore/grype/grype/db/v4/pkg/resolver/python"
- "github.com/anchore/grype/grype/db/v4/pkg/resolver/stock"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-func TestFromLanguage(t *testing.T) {
- tests := []struct {
- language syftPkg.Language
- result Resolver
- }{
- {
- language: syftPkg.Python,
- result: &python.Resolver{},
- },
- {
- language: syftPkg.Java,
- result: &java.Resolver{},
- },
- {
- language: syftPkg.Ruby,
- result: &stock.Resolver{},
- },
- {
- language: syftPkg.Dart,
- result: &stock.Resolver{},
- },
- {
- language: syftPkg.Rust,
- result: &stock.Resolver{},
- },
- {
- language: syftPkg.Go,
- result: &stock.Resolver{},
- },
- {
- language: syftPkg.JavaScript,
- result: &stock.Resolver{},
- },
- {
- language: syftPkg.Dotnet,
- result: &stock.Resolver{},
- },
- {
- language: syftPkg.PHP,
- result: &stock.Resolver{},
- },
- {
- language: syftPkg.Ruby,
- result: &stock.Resolver{},
- },
- {
- language: syftPkg.Language("something-new"),
- result: &stock.Resolver{},
- },
- }
-
- for _, test := range tests {
- result, err := FromLanguage(test.language)
- assert.NoError(t, err)
- assert.Equal(t, result, test.result)
- }
-}
diff --git a/grype/db/v4/pkg/resolver/python/resolver_test.go b/grype/db/v4/pkg/resolver/python/resolver_test.go
deleted file mode 100644
index f54aef42d0b..00000000000
--- a/grype/db/v4/pkg/resolver/python/resolver_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package python
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestResolver_Normalize(t *testing.T) {
- tests := []struct {
- packageName string
- normalized string
- }{
- {
- packageName: "PyYAML",
- normalized: "pyyaml",
- },
- {
- packageName: "oslo.concurrency",
- normalized: "oslo-concurrency",
- },
- {
- packageName: "",
- normalized: "",
- },
- {
- packageName: "test---1",
- normalized: "test-1",
- },
- {
- packageName: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
- normalized: "abcd-1234-xyzzz",
- },
- }
-
- resolver := Resolver{}
-
- for _, test := range tests {
- resolvedNames := resolver.Normalize(test.packageName)
- assert.Equal(t, resolvedNames, test.normalized)
- }
-}
diff --git a/grype/db/v4/pkg/resolver/resolver.go b/grype/db/v4/pkg/resolver/resolver.go
deleted file mode 100644
index bc253a253a9..00000000000
--- a/grype/db/v4/pkg/resolver/resolver.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package resolver
-
-import (
- grypePkg "github.com/anchore/grype/grype/pkg"
-)
-
-type Resolver interface {
- Normalize(string) string
- Resolve(p grypePkg.Package) []string
-}
diff --git a/grype/db/v4/pkg/resolver/stock/resolver.go b/grype/db/v4/pkg/resolver/stock/resolver.go
deleted file mode 100644
index c1e38411a9e..00000000000
--- a/grype/db/v4/pkg/resolver/stock/resolver.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package stock
-
-import (
- "strings"
-
- grypePkg "github.com/anchore/grype/grype/pkg"
-)
-
-type Resolver struct {
-}
-
-func (r *Resolver) Normalize(name string) string {
- return strings.ToLower(name)
-}
-
-func (r *Resolver) Resolve(p grypePkg.Package) []string {
- return []string{r.Normalize(p.Name)}
-}
diff --git a/grype/db/v4/pkg/resolver/stock/resolver_test.go b/grype/db/v4/pkg/resolver/stock/resolver_test.go
deleted file mode 100644
index 699b5817d0e..00000000000
--- a/grype/db/v4/pkg/resolver/stock/resolver_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package stock
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestResolver_Normalize(t *testing.T) {
- tests := []struct {
- packageName string
- normalized string
- }{
- {
- packageName: "PyYAML",
- normalized: "pyyaml",
- },
- {
- packageName: "oslo.concurrency",
- normalized: "oslo.concurrency",
- },
- {
- packageName: "",
- normalized: "",
- },
- {
- packageName: "test---1",
- normalized: "test---1",
- },
- {
- packageName: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
- normalized: "abcd.-__.--.-___.__.--1234____----....xyzzz",
- },
- }
-
- resolver := Resolver{}
-
- for _, test := range tests {
- resolvedNames := resolver.Normalize(test.packageName)
- assert.Equal(t, resolvedNames, test.normalized)
- }
-}
diff --git a/grype/db/v4/schema_version.go b/grype/db/v4/schema_version.go
deleted file mode 100644
index 3f74ff22fc5..00000000000
--- a/grype/db/v4/schema_version.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package v4
-
-const SchemaVersion = 4
diff --git a/grype/db/v4/store.go b/grype/db/v4/store.go
deleted file mode 100644
index b0ac18c1d0b..00000000000
--- a/grype/db/v4/store.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package v4
-
-type Store interface {
- StoreReader
- StoreWriter
- DBCloser
-}
-
-type StoreReader interface {
- IDReader
- DiffReader
- VulnerabilityStoreReader
- VulnerabilityMetadataStoreReader
- VulnerabilityMatchExclusionStoreReader
-}
-
-type StoreWriter interface {
- IDWriter
- VulnerabilityStoreWriter
- VulnerabilityMetadataStoreWriter
- VulnerabilityMatchExclusionStoreWriter
-}
-
-type DiffReader interface {
- DiffStore(s StoreReader) (*[]Diff, error)
-}
-
-type DBCloser interface {
- Close()
-}
diff --git a/grype/db/v4/store/diff.go b/grype/db/v4/store/diff.go
deleted file mode 100644
index 88ed92f5b5c..00000000000
--- a/grype/db/v4/store/diff.go
+++ /dev/null
@@ -1,305 +0,0 @@
-package store
-
-import (
- "github.com/wagoodman/go-partybus"
- "github.com/wagoodman/go-progress"
-
- v4 "github.com/anchore/grype/grype/db/v4"
- "github.com/anchore/grype/grype/event"
- "github.com/anchore/grype/grype/event/monitor"
- "github.com/anchore/grype/internal/bus"
-)
-
-type storeKey struct {
- id string
- namespace string
- packageName string
-}
-
-type PkgMap = map[storeKey][]string
-
-type storeVulnerabilityList struct {
- items map[storeKey][]storeVulnerability
- seen bool
-}
-type storeVulnerability struct {
- item *v4.Vulnerability
- seen bool
-}
-type storeMetadata struct {
- item *v4.VulnerabilityMetadata
- seen bool
-}
-
-// create manual progress bars for tracking the database diff's progress
-func trackDiff(total int64) (*progress.Manual, *progress.Manual, *progress.Stage) {
- stageProgress := &progress.Manual{}
- stageProgress.SetTotal(total)
- differencesDiscovered := &progress.Manual{}
- stager := &progress.Stage{}
-
- bus.Publish(partybus.Event{
- Type: event.DatabaseDiffingStarted,
- Value: monitor.DBDiff{
- Stager: stager,
- StageProgress: progress.Progressable(stageProgress),
- DifferencesDiscovered: progress.Monitorable(differencesDiscovered),
- },
- })
- return stageProgress, differencesDiscovered, stager
-}
-
-// creates a map from an unpackaged key to a list of all packages associated with it
-func buildVulnerabilityPkgsMap(models *[]v4.Vulnerability) *map[storeKey][]string {
- storeMap := make(map[storeKey][]string)
- for _, m := range *models {
- model := m
- k := getVulnerabilityParentKey(model)
- if storeVuln, exists := storeMap[k]; exists {
- storeMap[k] = append(storeVuln, model.PackageName)
- } else {
- storeMap[k] = []string{model.PackageName}
- }
- }
- return &storeMap
-}
-
-// creates a diff from the given key using the package maps information to populate
-// the relevant packages affected by the update
-func createDiff(baseStore, targetStore *PkgMap, key storeKey, reason v4.DiffReason) *v4.Diff {
- pkgMap := make(map[string]struct{})
-
- key.packageName = ""
- if baseStore != nil {
- if basePkgs, exists := (*baseStore)[key]; exists {
- for _, pkg := range basePkgs {
- pkgMap[pkg] = struct{}{}
- }
- }
- }
- if targetStore != nil {
- if targetPkgs, exists := (*targetStore)[key]; exists {
- for _, pkg := range targetPkgs {
- pkgMap[pkg] = struct{}{}
- }
- }
- }
- pkgs := []string{}
- for pkg := range pkgMap {
- pkgs = append(pkgs, pkg)
- }
-
- return &v4.Diff{
- Reason: reason,
- ID: key.id,
- Namespace: key.namespace,
- Packages: pkgs,
- }
-}
-
-// gets an unpackaged key from a vulnerability
-func getVulnerabilityParentKey(vuln v4.Vulnerability) storeKey {
- return storeKey{vuln.ID, vuln.Namespace, ""}
-}
-
-// gets a packaged key from a vulnerability
-func getVulnerabilityKey(vuln v4.Vulnerability) storeKey {
- return storeKey{vuln.ID, vuln.Namespace, vuln.PackageName}
-}
-
-type VulnerabilitySet struct {
- data map[storeKey]*storeVulnerabilityList
-}
-
-func NewVulnerabilitySet(models *[]v4.Vulnerability) *VulnerabilitySet {
- m := make(map[storeKey]*storeVulnerabilityList, len(*models))
- for _, mm := range *models {
- model := mm
- parentKey := getVulnerabilityParentKey(model)
- vulnKey := getVulnerabilityKey(model)
- if storeVuln, exists := m[parentKey]; exists {
- if kk, exists := storeVuln.items[vulnKey]; exists {
- storeVuln.items[vulnKey] = append(kk, storeVulnerability{
- item: &model,
- seen: false,
- })
- } else {
- storeVuln.items[vulnKey] = []storeVulnerability{{&model, false}}
- }
- } else {
- vuln := storeVulnerabilityList{
- items: make(map[storeKey][]storeVulnerability),
- seen: false,
- }
- vuln.items[vulnKey] = []storeVulnerability{{&model, false}}
- m[parentKey] = &vuln
- }
- }
- return &VulnerabilitySet{
- data: m,
- }
-}
-
-func (v *VulnerabilitySet) in(item v4.Vulnerability) bool {
- _, exists := v.data[getVulnerabilityParentKey(item)]
- return exists
-}
-
-func (v *VulnerabilitySet) match(item v4.Vulnerability) bool {
- if parent, exists := v.data[getVulnerabilityParentKey(item)]; exists {
- parent.seen = true
- key := getVulnerabilityKey(item)
- if children, exists := parent.items[key]; exists {
- for idx, child := range children {
- if item.Equal(*child.item) {
- children[idx].seen = true
- return true
- }
- }
- }
- }
- return false
-}
-
-func (v *VulnerabilitySet) getUnmatched() ([]storeKey, []storeKey) {
- notSeen := []storeKey{}
- notEntirelySeen := []storeKey{}
- for k, item := range v.data {
- if !item.seen {
- notSeen = append(notSeen, k)
- continue
- }
- componentLoop:
- for _, components := range item.items {
- for _, component := range components {
- if !component.seen {
- notEntirelySeen = append(notEntirelySeen, k)
- break componentLoop
- }
- }
- }
- }
- return notSeen, notEntirelySeen
-}
-
-func diffVulnerabilities(baseModels, targetModels *[]v4.Vulnerability, basePkgsMap, targetPkgsMap *PkgMap, differentItems *progress.Manual) *map[string]*v4.Diff {
- diffs := make(map[string]*v4.Diff)
- m := NewVulnerabilitySet(baseModels)
-
- for _, tModel := range *targetModels {
- targetModel := tModel
- k := getVulnerabilityKey(targetModel)
- if m.in(targetModel) {
- matched := m.match(targetModel)
- if !matched {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v4.DiffChanged)
- differentItems.Increment()
- }
- } else {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(nil, targetPkgsMap, k, v4.DiffAdded)
- differentItems.Increment()
- }
- }
- notSeen, partialSeen := m.getUnmatched()
- for _, k := range partialSeen {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v4.DiffChanged)
- differentItems.Increment()
- }
- for _, k := range notSeen {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, nil, k, v4.DiffRemoved)
- differentItems.Increment()
- }
-
- return &diffs
-}
-
-type MetadataSet struct {
- data map[storeKey]*storeMetadata
-}
-
-func NewMetadataSet(models *[]v4.VulnerabilityMetadata) *MetadataSet {
- m := make(map[storeKey]*storeMetadata, len(*models))
- for _, mm := range *models {
- model := mm
- m[getMetadataKey(model)] = &storeMetadata{
- item: &model,
- seen: false,
- }
- }
- return &MetadataSet{
- data: m,
- }
-}
-
-func (v *MetadataSet) in(item v4.VulnerabilityMetadata) bool {
- _, exists := v.data[getMetadataKey(item)]
- return exists
-}
-
-func (v *MetadataSet) match(item v4.VulnerabilityMetadata) bool {
- if baseModel, exists := v.data[getMetadataKey(item)]; exists {
- baseModel.seen = true
- return baseModel.item.Equal(item)
- }
- return false
-}
-
-func (v *MetadataSet) getUnmatched() []storeKey {
- notSeen := []storeKey{}
- for k, item := range v.data {
- if !item.seen {
- notSeen = append(notSeen, k)
- }
- }
- return notSeen
-}
-
-func diffVulnerabilityMetadata(baseModels, targetModels *[]v4.VulnerabilityMetadata, basePkgsMap, targetPkgsMap *PkgMap, differentItems *progress.Manual) *map[string]*v4.Diff {
- diffs := make(map[string]*v4.Diff)
- m := NewMetadataSet(baseModels)
-
- for _, tModel := range *targetModels {
- targetModel := tModel
- k := getMetadataKey(targetModel)
- if m.in(targetModel) {
- if !m.match(targetModel) {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v4.DiffChanged)
- differentItems.Increment()
- }
- } else {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(nil, targetPkgsMap, k, v4.DiffAdded)
- differentItems.Increment()
- }
- }
- for _, k := range m.getUnmatched() {
- if _, exists := diffs[k.id+k.namespace]; exists {
- continue
- }
- diffs[k.id+k.namespace] = createDiff(basePkgsMap, nil, k, v4.DiffRemoved)
- differentItems.Increment()
- }
-
- return &diffs
-}
-
-func getMetadataKey(metadata v4.VulnerabilityMetadata) storeKey {
- return storeKey{metadata.ID, metadata.Namespace, ""}
-}
diff --git a/grype/db/v4/store/diff_test.go b/grype/db/v4/store/diff_test.go
deleted file mode 100644
index 41b8dd85bd4..00000000000
--- a/grype/db/v4/store/diff_test.go
+++ /dev/null
@@ -1,231 +0,0 @@
-package store
-
-import (
- "sort"
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- v4 "github.com/anchore/grype/grype/db/v4"
-)
-
-func Test_GetAllVulnerabilities(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- //WHEN
- result, err := s.GetAllVulnerabilities()
-
- //THEN
- assert.NotNil(t, result)
- assert.NoError(t, err)
-}
-
-func Test_GetAllVulnerabilityMetadata(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- //WHEN
- result, err := s.GetAllVulnerabilityMetadata()
-
- //THEN
- assert.NotNil(t, result)
- assert.NoError(t, err)
-}
-
-func Test_Diff_Vulnerabilities(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
-
- s1, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
- dbTempFile = t.TempDir()
- s2, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- baseVulns := []v4.Vulnerability{
- {
- Namespace: "github:language:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 2.0 >= 1.29",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "github:language:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- PackageName: "npm:axios",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"},
- Fix: v4.Fix{
- State: v4.UnknownFixState,
- },
- },
- }
- targetVulns := []v4.Vulnerability{
- {
- Namespace: "github:language:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 2.0 >= 1.29",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "github:language:go",
- ID: "GHSA-....-....",
- PackageName: "hashicorp:nomad",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:golang:hashicorp:nomad:*:*:*:*:*"},
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- PackageName: "npm:axios",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"},
- Fix: v4.Fix{
- State: v4.WontFixState,
- },
- },
- }
- expectedDiffs := []v4.Diff{
- {
- Reason: v4.DiffChanged,
- ID: "CVE-123-4567",
- Namespace: "github:language:python",
- Packages: []string{"pypi:requests"},
- },
- {
- Reason: v4.DiffChanged,
- ID: "CVE-123-7654",
- Namespace: "npm",
- Packages: []string{"npm:axios"},
- },
- {
- Reason: v4.DiffAdded,
- ID: "GHSA-....-....",
- Namespace: "github:language:go",
- Packages: []string{"hashicorp:nomad"},
- },
- }
-
- for _, vuln := range baseVulns {
- s1.AddVulnerability(vuln)
- }
- for _, vuln := range targetVulns {
- s2.AddVulnerability(vuln)
- }
-
- //WHEN
- result, err := s1.DiffStore(s2)
- sort.SliceStable(*result, func(i, j int) bool {
- return (*result)[i].ID < (*result)[j].ID
- })
-
- //THEN
- assert.NoError(t, err)
- assert.Equal(t, expectedDiffs, *result)
-}
-
-func Test_Diff_Metadata(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
-
- s1, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
- dbTempFile = t.TempDir()
-
- s2, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- baseVulns := []v4.VulnerabilityMetadata{
- {
- Namespace: "github:language:python",
- ID: "CVE-123-4567",
- DataSource: "nvd",
- },
- {
- Namespace: "github:language:python",
- ID: "CVE-123-4567",
- DataSource: "nvd",
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- DataSource: "nvd",
- },
- }
- targetVulns := []v4.VulnerabilityMetadata{
- {
- Namespace: "github:language:go",
- ID: "GHSA-....-....",
- DataSource: "nvd",
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- DataSource: "vulndb",
- },
- }
- expectedDiffs := []v4.Diff{
- {
- Reason: v4.DiffRemoved,
- ID: "CVE-123-4567",
- Namespace: "github:language:python",
- Packages: []string{},
- },
- {
- Reason: v4.DiffChanged,
- ID: "CVE-123-7654",
- Namespace: "npm",
- Packages: []string{},
- },
- {
- Reason: v4.DiffAdded,
- ID: "GHSA-....-....",
- Namespace: "github:language:go",
- Packages: []string{},
- },
- }
-
- for _, vuln := range baseVulns {
- s1.AddVulnerabilityMetadata(vuln)
- }
- for _, vuln := range targetVulns {
- s2.AddVulnerabilityMetadata(vuln)
- }
-
- //WHEN
- result, err := s1.DiffStore(s2)
-
- //THEN
- sort.SliceStable(*result, func(i, j int) bool {
- return (*result)[i].ID < (*result)[j].ID
- })
-
- assert.NoError(t, err)
- assert.Equal(t, expectedDiffs, *result)
-}
diff --git a/grype/db/v4/store/model/id.go b/grype/db/v4/store/model/id.go
deleted file mode 100644
index b6c0520dc54..00000000000
--- a/grype/db/v4/store/model/id.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package model
-
-import (
- "fmt"
- "time"
-
- v4 "github.com/anchore/grype/grype/db/v4"
-)
-
-const (
- IDTableName = "id"
-)
-
-type IDModel struct {
- BuildTimestamp string `gorm:"column:build_timestamp"`
- SchemaVersion int `gorm:"column:schema_version"`
-}
-
-func NewIDModel(id v4.ID) IDModel {
- return IDModel{
- BuildTimestamp: id.BuildTimestamp.Format(time.RFC3339Nano),
- SchemaVersion: id.SchemaVersion,
- }
-}
-
-func (IDModel) TableName() string {
- return IDTableName
-}
-
-func (m *IDModel) Inflate() (v4.ID, error) {
- buildTime, err := time.Parse(time.RFC3339Nano, m.BuildTimestamp)
- if err != nil {
- return v4.ID{}, fmt.Errorf("unable to parse build timestamp (%+v): %w", m.BuildTimestamp, err)
- }
-
- return v4.ID{
- BuildTimestamp: buildTime,
- SchemaVersion: m.SchemaVersion,
- }, nil
-}
diff --git a/grype/db/v4/store/model/vulnerability.go b/grype/db/v4/store/model/vulnerability.go
deleted file mode 100644
index d229c036f4d..00000000000
--- a/grype/db/v4/store/model/vulnerability.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package model
-
-import (
- "encoding/json"
- "fmt"
-
- sqlite "github.com/anchore/grype/grype/db/internal/sqlite"
- v4 "github.com/anchore/grype/grype/db/v4"
-)
-
-const (
- VulnerabilityTableName = "vulnerability"
- GetVulnerabilityIndexName = "get_vulnerability_index"
-)
-
-// VulnerabilityModel is a struct used to serialize db.Vulnerability information into a sqlite3 DB.
-type VulnerabilityModel struct {
- PK uint64 `gorm:"primary_key;auto_increment;"`
- ID string `gorm:"column:id"`
- PackageName string `gorm:"column:package_name; index:get_vulnerability_index"`
- Namespace string `gorm:"column:namespace; index:get_vulnerability_index"`
- VersionConstraint string `gorm:"column:version_constraint"`
- VersionFormat string `gorm:"column:version_format"`
- CPEs sqlite.NullString `gorm:"column:cpes; default:null"`
- RelatedVulnerabilities sqlite.NullString `gorm:"column:related_vulnerabilities; default:null"`
- FixedInVersions sqlite.NullString `gorm:"column:fixed_in_versions; default:null"`
- FixState string `gorm:"column:fix_state"`
- Advisories sqlite.NullString `gorm:"column:advisories; default:null"`
-}
-
-// NewVulnerabilityModel generates a new model from a db.Vulnerability struct.
-func NewVulnerabilityModel(vulnerability v4.Vulnerability) VulnerabilityModel {
- return VulnerabilityModel{
- ID: vulnerability.ID,
- PackageName: vulnerability.PackageName,
- Namespace: vulnerability.Namespace,
- VersionConstraint: vulnerability.VersionConstraint,
- VersionFormat: vulnerability.VersionFormat,
- FixedInVersions: sqlite.ToNullString(vulnerability.Fix.Versions),
- FixState: string(vulnerability.Fix.State),
- Advisories: sqlite.ToNullString(vulnerability.Advisories),
- CPEs: sqlite.ToNullString(vulnerability.CPEs),
- RelatedVulnerabilities: sqlite.ToNullString(vulnerability.RelatedVulnerabilities),
- }
-}
-
-// TableName returns the table which all db.Vulnerability model instances are stored into.
-func (VulnerabilityModel) TableName() string {
- return VulnerabilityTableName
-}
-
-// Inflate generates a db.Vulnerability object from the serialized model instance.
-func (m *VulnerabilityModel) Inflate() (v4.Vulnerability, error) {
- var cpes []string
- err := json.Unmarshal(m.CPEs.ToByteSlice(), &cpes)
- if err != nil {
- return v4.Vulnerability{}, fmt.Errorf("unable to unmarshal CPEs (%+v): %w", m.CPEs, err)
- }
-
- var related []v4.VulnerabilityReference
- err = json.Unmarshal(m.RelatedVulnerabilities.ToByteSlice(), &related)
- if err != nil {
- return v4.Vulnerability{}, fmt.Errorf("unable to unmarshal related vulnerabilities (%+v): %w", m.RelatedVulnerabilities, err)
- }
-
- var advisories []v4.Advisory
-
- err = json.Unmarshal(m.Advisories.ToByteSlice(), &advisories)
- if err != nil {
- return v4.Vulnerability{}, fmt.Errorf("unable to unmarshal advisories (%+v): %w", m.Advisories, err)
- }
-
- var versions []string
- err = json.Unmarshal(m.FixedInVersions.ToByteSlice(), &versions)
- if err != nil {
- return v4.Vulnerability{}, fmt.Errorf("unable to unmarshal versions (%+v): %w", m.FixedInVersions, err)
- }
-
- return v4.Vulnerability{
- ID: m.ID,
- PackageName: m.PackageName,
- Namespace: m.Namespace,
- VersionConstraint: m.VersionConstraint,
- VersionFormat: m.VersionFormat,
- CPEs: cpes,
- RelatedVulnerabilities: related,
- Fix: v4.Fix{
- Versions: versions,
- State: v4.FixState(m.FixState),
- },
- Advisories: advisories,
- }, nil
-}
diff --git a/grype/db/v4/store/model/vulnerability_match_exclusion.go b/grype/db/v4/store/model/vulnerability_match_exclusion.go
deleted file mode 100644
index 029bf4c783b..00000000000
--- a/grype/db/v4/store/model/vulnerability_match_exclusion.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package model
-
-import (
- "encoding/json"
- "fmt"
-
- "github.com/anchore/grype/grype/db/internal/sqlite"
- v4 "github.com/anchore/grype/grype/db/v4"
- "github.com/anchore/grype/internal/log"
-)
-
-const (
- VulnerabilityMatchExclusionTableName = "vulnerability_match_exclusion"
- GetVulnerabilityMatchExclusionIndexName = "get_vulnerability_match_exclusion_index"
-)
-
-// VulnerabilityMatchExclusionModel is a struct used to serialize db.VulnerabilityMatchExclusion information into a sqlite3 DB.
-type VulnerabilityMatchExclusionModel struct {
- PK uint64 `gorm:"primary_key;auto_increment;"`
- ID string `gorm:"column:id; index:get_vulnerability_match_exclusion_index"`
- Constraints sqlite.NullString `gorm:"column:constraints; default:null"`
- Justification string `gorm:"column:justification"`
-}
-
-// NewVulnerabilityMatchExclusionModel generates a new model from a db.VulnerabilityMatchExclusion struct.
-func NewVulnerabilityMatchExclusionModel(v v4.VulnerabilityMatchExclusion) VulnerabilityMatchExclusionModel {
- return VulnerabilityMatchExclusionModel{
- ID: v.ID,
- Constraints: sqlite.ToNullString(v.Constraints),
- Justification: v.Justification,
- }
-}
-
-// TableName returns the table which all db.VulnerabilityMatchExclusion model instances are stored into.
-func (VulnerabilityMatchExclusionModel) TableName() string {
- return VulnerabilityMatchExclusionTableName
-}
-
-// Inflate generates a db.VulnerabilityMatchExclusion object from the serialized model instance.
-func (m *VulnerabilityMatchExclusionModel) Inflate() (*v4.VulnerabilityMatchExclusion, error) {
- // It's important that we only utilise exclusion constraints that are compatible with this version of Grype,
- // so if any unknown fields are encountered then ignore that constraint.
-
- var constraints []v4.VulnerabilityMatchExclusionConstraint
- err := json.Unmarshal(m.Constraints.ToByteSlice(), &constraints)
- if err != nil {
- return nil, fmt.Errorf("unable to unmarshal vulnerability match exclusion constraints (%+v): %w", m.Constraints, err)
- }
-
- var compatibleConstraints []v4.VulnerabilityMatchExclusionConstraint
-
- if len(constraints) > 0 {
- for _, c := range constraints {
- if !c.Usable() {
- log.Debugf("skipping incompatible vulnerability match constraint for vuln id=%s, constraint=%+v", m.ID, c)
- } else {
- compatibleConstraints = append(compatibleConstraints, c)
- }
- }
-
- // If there were constraints and none were compatible, the entire record is not usable by this version of Grype
- if len(compatibleConstraints) == 0 {
- return nil, nil
- }
- }
-
- return &v4.VulnerabilityMatchExclusion{
- ID: m.ID,
- Constraints: compatibleConstraints,
- Justification: m.Justification,
- }, nil
-}
diff --git a/grype/db/v4/store/model/vulnerability_match_exclusion_test.go b/grype/db/v4/store/model/vulnerability_match_exclusion_test.go
deleted file mode 100644
index 238557cb643..00000000000
--- a/grype/db/v4/store/model/vulnerability_match_exclusion_test.go
+++ /dev/null
@@ -1,201 +0,0 @@
-package model
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- "github.com/anchore/grype/grype/db/internal/sqlite"
- v4 "github.com/anchore/grype/grype/db/v4"
-)
-
-func TestVulnerabilityMatchExclusionModel_Inflate(t *testing.T) {
- tests := []struct {
- name string
- record *VulnerabilityMatchExclusionModel
- result *v4.VulnerabilityMatchExclusion
- }{
- {
- name: "Nil constraint",
- record: &VulnerabilityMatchExclusionModel{
- PK: 0,
- ID: "CVE-12345",
- Constraints: sqlite.ToNullString(nil),
- Justification: "Who really knows?",
- },
- result: &v4.VulnerabilityMatchExclusion{
- ID: "CVE-12345",
- Constraints: nil,
- Justification: "Who really knows?",
- },
- },
- {
- name: "Empty constraint array",
- record: &VulnerabilityMatchExclusionModel{
- PK: 0,
- ID: "CVE-919",
- Constraints: sqlite.NewNullString(`[]`, true),
- Justification: "Always ignore",
- },
- result: &v4.VulnerabilityMatchExclusion{
- ID: "CVE-919",
- Constraints: nil,
- Justification: "Always ignore",
- },
- },
- {
- name: "Single constraint",
- record: &VulnerabilityMatchExclusionModel{
- PK: 0,
- ID: "CVE-919",
- Constraints: sqlite.NewNullString(`[{"vulnerability":{"namespace":"nvd:cpe"},"package":{"language":"python"}}]`, true),
- Justification: "Python packages are not vulnerable",
- },
- result: &v4.VulnerabilityMatchExclusion{
- ID: "CVE-919",
- Constraints: []v4.VulnerabilityMatchExclusionConstraint{
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "nvd:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Language: "python",
- },
- },
- },
- Justification: "Python packages are not vulnerable",
- },
- },
- {
- name: "Single unusable constraint with unknown vulnerability constraint field",
- record: &VulnerabilityMatchExclusionModel{
- PK: 0,
- ID: "CVE-919",
- Constraints: sqlite.NewNullString(`[{"vulnerability":{"namespace":"nvd:cpe","something_new":"1234"}}]`, true),
- Justification: "Python packages are not vulnerable",
- },
- result: nil,
- },
- {
- name: "Single unusable constraint with unknown package constraint fields",
- record: &VulnerabilityMatchExclusionModel{
- PK: 0,
- ID: "CVE-919",
- Constraints: sqlite.NewNullString(`[{"package":{"name":"jim","another_field":"1234","x_y_z":"abc"}}]`, true),
- Justification: "Python packages are not vulnerable",
- },
- result: nil,
- },
- {
- name: "Single unusable constraint with unknown root-level constraint fields",
- record: &VulnerabilityMatchExclusionModel{
- PK: 0,
- ID: "CVE-919",
- Constraints: sqlite.NewNullString(`[{"x_y_z":{"name":"jim","another_field":"1234","x_y_z":"abc"},"package":{"name":"jim","another_field":"1234","x_y_z":"abc"}}]`, true),
- Justification: "Python packages are not vulnerable",
- },
- result: nil,
- },
- {
- name: "Multiple usable constraints",
- record: &VulnerabilityMatchExclusionModel{
- PK: 0,
- ID: "CVE-2025-152345",
- Constraints: sqlite.NewNullString(`[{"vulnerability":{"namespace":"abc.xyz:language:ruby","fix_state":"wont-fix"},"package":{"language":"ruby","type":"not-gem"}},{"package":{"language":"python","version":"1000.0.1"}},{"vulnerability":{"namespace":"nvd:cpe"}},{"vulnerability":{"namespace":"nvd:cpe"},"package":{"name":"x"}},{"package":{"location":"/bin/x"}}]`, true),
- Justification: "Python packages are not vulnerable",
- },
- result: &v4.VulnerabilityMatchExclusion{
- ID: "CVE-2025-152345",
- Constraints: []v4.VulnerabilityMatchExclusionConstraint{
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "abc.xyz:language:ruby",
- FixState: "wont-fix",
- },
- Package: v4.PackageExclusionConstraint{
- Language: "ruby",
- Type: "not-gem",
- },
- },
- {
- Package: v4.PackageExclusionConstraint{
- Language: "python",
- Version: "1000.0.1",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "nvd:cpe",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "nvd:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Name: "x",
- },
- },
- {
- Package: v4.PackageExclusionConstraint{
- Location: "/bin/x",
- },
- },
- },
- Justification: "Python packages are not vulnerable",
- },
- },
- {
- name: "Multiple constraints with some unusable",
- record: &VulnerabilityMatchExclusionModel{
- PK: 0,
- ID: "CVE-2025-152345",
- Constraints: sqlite.NewNullString(`[{"a_b_c": "x","vulnerability":{"namespace":"abc.xyz:language:ruby","fix_state":"wont-fix"},"package":{"language":"ruby","type":"not-gem"}},{"package":{"language":"python","version":"1000.0.1"}},{"vulnerability":{"namespace":"nvd:cpe"}},{"vulnerability":{"namespace":"nvd:cpe"},"package":{"name":"x"}},{"package":{"location":"/bin/x","nnnn":"no"}}]`, true),
- Justification: "Python packages are not vulnerable",
- },
- result: &v4.VulnerabilityMatchExclusion{
- ID: "CVE-2025-152345",
- Constraints: []v4.VulnerabilityMatchExclusionConstraint{
- {
- Package: v4.PackageExclusionConstraint{
- Language: "python",
- Version: "1000.0.1",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "nvd:cpe",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "nvd:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Name: "x",
- },
- },
- },
- Justification: "Python packages are not vulnerable",
- },
- },
- {
- name: "Multiple constraints all unusable",
- record: &VulnerabilityMatchExclusionModel{
- PK: 0,
- ID: "CVE-2025-152345",
- Constraints: sqlite.NewNullString(`[{"a_b_c": "x","vulnerability":{"namespace":"abc.xyz:language:ruby","fix_state":"wont-fix"},"package":{"language":"ruby","type":"not-gem"}},{"a_b_c": "x","package":{"language":"python","version":"1000.0.1"}},{"a_b_c": "x","vulnerability":{"namespace":"nvd:cpe"}},{"a_b_c": "x","vulnerability":{"namespace":"nvd:cpe"},"package":{"name":"x"}},{"package":{"location":"/bin/x","nnnn":"no"}}]`, true),
- Justification: "Python packages are not vulnerable",
- },
- result: nil,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- result, err := test.record.Inflate()
- assert.NoError(t, err)
- assert.Equal(t, test.result, result)
- })
- }
-}
diff --git a/grype/db/v4/store/model/vulnerability_metadata.go b/grype/db/v4/store/model/vulnerability_metadata.go
deleted file mode 100644
index c576e48630e..00000000000
--- a/grype/db/v4/store/model/vulnerability_metadata.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package model
-
-import (
- "encoding/json"
- "fmt"
-
- sqlite "github.com/anchore/grype/grype/db/internal/sqlite"
- v4 "github.com/anchore/grype/grype/db/v4"
-)
-
-const (
- VulnerabilityMetadataTableName = "vulnerability_metadata"
-)
-
-// VulnerabilityMetadataModel is a struct used to serialize db.VulnerabilityMetadata information into a sqlite3 DB.
-type VulnerabilityMetadataModel struct {
- ID string `gorm:"primary_key; column:id;"`
- Namespace string `gorm:"primary_key; column:namespace;"`
- DataSource string `gorm:"column:data_source"`
- RecordSource string `gorm:"column:record_source"`
- Severity string `gorm:"column:severity"`
- URLs sqlite.NullString `gorm:"column:urls; default:null"`
- Description string `gorm:"column:description"`
- Cvss sqlite.NullString `gorm:"column:cvss; default:null"`
-}
-
-// NewVulnerabilityMetadataModel generates a new model from a db.VulnerabilityMetadata struct.
-func NewVulnerabilityMetadataModel(metadata v4.VulnerabilityMetadata) VulnerabilityMetadataModel {
- if metadata.Cvss == nil {
- metadata.Cvss = make([]v4.Cvss, 0)
- }
-
- return VulnerabilityMetadataModel{
- ID: metadata.ID,
- Namespace: metadata.Namespace,
- DataSource: metadata.DataSource,
- RecordSource: metadata.RecordSource,
- Severity: metadata.Severity,
- URLs: sqlite.ToNullString(metadata.URLs),
- Description: metadata.Description,
- Cvss: sqlite.ToNullString(metadata.Cvss),
- }
-}
-
-// TableName returns the table which all db.VulnerabilityMetadata model instances are stored into.
-func (VulnerabilityMetadataModel) TableName() string {
- return VulnerabilityMetadataTableName
-}
-
-// Inflate generates a db.VulnerabilityMetadataModel object from the serialized model instance.
-func (m *VulnerabilityMetadataModel) Inflate() (v4.VulnerabilityMetadata, error) {
- var links []string
- var cvss []v4.Cvss
-
- if err := json.Unmarshal(m.URLs.ToByteSlice(), &links); err != nil {
- return v4.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal URLs (%+v): %w", m.URLs, err)
- }
-
- err := json.Unmarshal(m.Cvss.ToByteSlice(), &cvss)
- if err != nil {
- return v4.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvss data (%+v): %w", m.Cvss, err)
- }
-
- return v4.VulnerabilityMetadata{
- ID: m.ID,
- Namespace: m.Namespace,
- DataSource: m.DataSource,
- RecordSource: m.RecordSource,
- Severity: m.Severity,
- URLs: links,
- Description: m.Description,
- Cvss: cvss,
- }, nil
-}
diff --git a/grype/db/v4/store/store.go b/grype/db/v4/store/store.go
deleted file mode 100644
index 01b005590f3..00000000000
--- a/grype/db/v4/store/store.go
+++ /dev/null
@@ -1,362 +0,0 @@
-package store
-
-import (
- "fmt"
- "sort"
-
- "github.com/go-test/deep"
- "gorm.io/gorm"
-
- "github.com/anchore/grype/grype/db/internal/gormadapter"
- v4 "github.com/anchore/grype/grype/db/v4"
- "github.com/anchore/grype/grype/db/v4/store/model"
- "github.com/anchore/grype/internal/stringutil"
- _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
-)
-
-// store holds an instance of the database connection
-type store struct {
- db *gorm.DB
-}
-
-// New creates a new instance of the store.
-func New(dbFilePath string, overwrite bool) (v4.Store, error) {
- db, err := gormadapter.Open(dbFilePath, overwrite)
- if err != nil {
- return nil, err
- }
-
- if overwrite {
- // TODO: automigrate could write to the database,
- // we should be validating the database is the correct database based on the version in the ID table before
- // automigrating
- if err := db.AutoMigrate(&model.IDModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate ID model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityMetadataModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability Metadata model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityMatchExclusionModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability Match Exclusion model: %w", err)
- }
- }
-
- return &store{
- db: db,
- }, nil
-}
-
-// GetID fetches the metadata about the databases schema version and build time.
-func (s *store) GetID() (*v4.ID, error) {
- var models []model.IDModel
- result := s.db.Find(&models)
- if result.Error != nil {
- return nil, result.Error
- }
-
- switch {
- case len(models) > 1:
- return nil, fmt.Errorf("found multiple DB IDs")
- case len(models) == 1:
- id, err := models[0].Inflate()
- if err != nil {
- return nil, err
- }
- return &id, nil
- }
-
- return nil, nil
-}
-
-// SetID stores the databases schema version and build time.
-func (s *store) SetID(id v4.ID) error {
- var ids []model.IDModel
-
- // replace the existing ID with the given one
- s.db.Find(&ids).Delete(&ids)
-
- m := model.NewIDModel(id)
- result := s.db.Create(&m)
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add id (%d rows affected)", result.RowsAffected)
- }
-
- return result.Error
-}
-
-// GetVulnerabilityNamespaces retrieves all possible namespaces from the database.
-func (s *store) GetVulnerabilityNamespaces() ([]string, error) {
- var names []string
- result := s.db.Model(&model.VulnerabilityMetadataModel{}).Distinct().Pluck("namespace", &names)
- return names, result.Error
-}
-
-// GetVulnerability retrieves vulnerabilities by namespace and package
-func (s *store) GetVulnerability(namespace, packageName string) ([]v4.Vulnerability, error) {
- var models []model.VulnerabilityModel
-
- result := s.db.Where("namespace = ? AND package_name = ?", namespace, packageName).Find(&models)
-
- var vulnerabilities = make([]v4.Vulnerability, len(models))
- for idx, m := range models {
- vulnerability, err := m.Inflate()
- if err != nil {
- return nil, err
- }
- vulnerabilities[idx] = vulnerability
- }
-
- return vulnerabilities, result.Error
-}
-
-// AddVulnerability saves one or more vulnerabilities into the sqlite3 store.
-func (s *store) AddVulnerability(vulnerabilities ...v4.Vulnerability) error {
- for _, vulnerability := range vulnerabilities {
- m := model.NewVulnerabilityModel(vulnerability)
-
- result := s.db.Create(&m)
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add vulnerability (%d rows affected)", result.RowsAffected)
- }
- }
- return nil
-}
-
-// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
-func (s *store) GetVulnerabilityMetadata(id, namespace string) (*v4.VulnerabilityMetadata, error) {
- var models []model.VulnerabilityMetadataModel
-
- result := s.db.Where(&model.VulnerabilityMetadataModel{ID: id, Namespace: namespace}).Find(&models)
- if result.Error != nil {
- return nil, result.Error
- }
-
- switch {
- case len(models) > 1:
- return nil, fmt.Errorf("found multiple metadatas for single ID=%q Namespace=%q", id, namespace)
- case len(models) == 1:
- metadata, err := models[0].Inflate()
- if err != nil {
- return nil, err
- }
-
- return &metadata, nil
- }
-
- return nil, nil
-}
-
-// AddVulnerabilityMetadata stores one or more vulnerability metadata models into the sqlite DB.
-//
-//nolint:gocognit
-func (s *store) AddVulnerabilityMetadata(metadata ...v4.VulnerabilityMetadata) error {
- for _, m := range metadata {
- existing, err := s.GetVulnerabilityMetadata(m.ID, m.Namespace)
- if err != nil {
- return fmt.Errorf("failed to verify existing entry: %w", err)
- }
-
- if existing != nil {
- // merge with the existing entry
-
- switch {
- case existing.Severity != m.Severity:
- return fmt.Errorf("existing metadata has mismatched severity (%q!=%q)", existing.Severity, m.Severity)
- case existing.Description != m.Description:
- return fmt.Errorf("existing metadata has mismatched description (%q!=%q)", existing.Description, m.Description)
- }
-
- incoming:
- // go through all incoming CVSS and see if they are already stored.
- // If they exist already in the database then skip adding them,
- // preventing a duplicate
- for _, incomingCvss := range m.Cvss {
- for _, existingCvss := range existing.Cvss {
- if len(deep.Equal(incomingCvss, existingCvss)) == 0 {
- // duplicate found, so incoming CVSS shouldn't get added
- continue incoming
- }
- }
- // a duplicate CVSS entry wasn't found, so append the incoming CVSS
- existing.Cvss = append(existing.Cvss, incomingCvss)
- }
-
- links := stringutil.NewStringSetFromSlice(existing.URLs)
- for _, l := range m.URLs {
- links.Add(l)
- }
-
- existing.URLs = links.ToSlice()
- sort.Strings(existing.URLs)
-
- newModel := model.NewVulnerabilityMetadataModel(*existing)
- result := s.db.Save(&newModel)
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to merge vulnerability metadata (%d rows affected)", result.RowsAffected)
- }
-
- if result.Error != nil {
- return result.Error
- }
- } else {
- // this is a new entry
- newModel := model.NewVulnerabilityMetadataModel(m)
- result := s.db.Create(&newModel)
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add vulnerability metadata (%d rows affected)", result.RowsAffected)
- }
- }
- }
- return nil
-}
-
-// GetVulnerabilityMatchExclusion retrieves one or more vulnerability match exclusion records given a vulnerability identifier.
-func (s *store) GetVulnerabilityMatchExclusion(id string) ([]v4.VulnerabilityMatchExclusion, error) {
- var models []model.VulnerabilityMatchExclusionModel
-
- result := s.db.Where("id = ?", id).Find(&models)
-
- var exclusions []v4.VulnerabilityMatchExclusion
- for _, m := range models {
- exclusion, err := m.Inflate()
- if err != nil {
- return nil, err
- }
-
- if exclusion != nil {
- exclusions = append(exclusions, *exclusion)
- }
- }
-
- return exclusions, result.Error
-}
-
-// AddVulnerabilityMatchExclusion saves one or more vulnerability match exclusion records into the sqlite3 store.
-func (s *store) AddVulnerabilityMatchExclusion(exclusions ...v4.VulnerabilityMatchExclusion) error {
- for _, exclusion := range exclusions {
- m := model.NewVulnerabilityMatchExclusionModel(exclusion)
-
- result := s.db.Create(&m)
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected != 1 {
- return fmt.Errorf("unable to add vulnerability match exclusion (%d rows affected)", result.RowsAffected)
- }
- }
-
- return nil
-}
-
-func (s *store) Close() {
- s.db.Exec("VACUUM;")
-
- sqlDB, err := s.db.DB()
- if err != nil {
- _ = sqlDB.Close()
- }
-}
-
-// GetAllVulnerabilities gets all vulnerabilities in the database
-func (s *store) GetAllVulnerabilities() (*[]v4.Vulnerability, error) {
- var models []model.VulnerabilityModel
- if result := s.db.Find(&models); result.Error != nil {
- return nil, result.Error
- }
- vulns := make([]v4.Vulnerability, len(models))
- for idx, m := range models {
- vuln, err := m.Inflate()
- if err != nil {
- return nil, err
- }
- vulns[idx] = vuln
- }
- return &vulns, nil
-}
-
-// GetAllVulnerabilityMetadata gets all vulnerability metadata in the database
-func (s *store) GetAllVulnerabilityMetadata() (*[]v4.VulnerabilityMetadata, error) {
- var models []model.VulnerabilityMetadataModel
- if result := s.db.Find(&models); result.Error != nil {
- return nil, result.Error
- }
- metadata := make([]v4.VulnerabilityMetadata, len(models))
- for idx, m := range models {
- data, err := m.Inflate()
- if err != nil {
- return nil, err
- }
- metadata[idx] = data
- }
- return &metadata, nil
-}
-
-// DiffStore creates a diff between the current sql database and the given store
-func (s *store) DiffStore(targetStore v4.StoreReader) (*[]v4.Diff, error) {
- // 7 stages, one for each step of the diff process (stages)
- rowsProgress, diffItems, stager := trackDiff(7)
-
- stager.Current = "reading target vulnerabilities"
- targetVulns, err := targetStore.GetAllVulnerabilities()
- rowsProgress.Increment()
- if err != nil {
- return nil, err
- }
-
- stager.Current = "reading base vulnerabilities"
- baseVulns, err := s.GetAllVulnerabilities()
- rowsProgress.Increment()
- if err != nil {
- return nil, err
- }
-
- stager.Current = "preparing"
- baseVulnPkgMap := buildVulnerabilityPkgsMap(baseVulns)
- targetVulnPkgMap := buildVulnerabilityPkgsMap(targetVulns)
-
- stager.Current = "comparing vulnerabilities"
- allDiffsMap := diffVulnerabilities(baseVulns, targetVulns, baseVulnPkgMap, targetVulnPkgMap, diffItems)
-
- stager.Current = "reading base metadata"
- baseMetadata, err := s.GetAllVulnerabilityMetadata()
- if err != nil {
- return nil, err
- }
- rowsProgress.Increment()
-
- stager.Current = "reading target metadata"
- targetMetadata, err := targetStore.GetAllVulnerabilityMetadata()
- if err != nil {
- return nil, err
- }
- rowsProgress.Increment()
-
- stager.Current = "comparing metadata"
- metaDiffsMap := diffVulnerabilityMetadata(baseMetadata, targetMetadata, baseVulnPkgMap, targetVulnPkgMap, diffItems)
- for k, diff := range *metaDiffsMap {
- (*allDiffsMap)[k] = diff
- }
- allDiffs := []v4.Diff{}
- for _, diff := range *allDiffsMap {
- allDiffs = append(allDiffs, *diff)
- }
-
- rowsProgress.SetCompleted()
- diffItems.SetCompleted()
-
- return &allDiffs, nil
-}
diff --git a/grype/db/v4/store/store_test.go b/grype/db/v4/store/store_test.go
deleted file mode 100644
index e8bee074a69..00000000000
--- a/grype/db/v4/store/store_test.go
+++ /dev/null
@@ -1,1377 +0,0 @@
-package store
-
-import (
- "encoding/json"
- "sort"
- "testing"
- "time"
-
- "github.com/go-test/deep"
- "github.com/stretchr/testify/assert"
-
- v4 "github.com/anchore/grype/grype/db/v4"
- "github.com/anchore/grype/grype/db/v4/store/model"
-)
-
-func assertIDReader(t *testing.T, reader v4.IDReader, expected v4.ID) {
- t.Helper()
- if actual, err := reader.GetID(); err != nil {
- t.Fatalf("failed to get ID: %+v", err)
- } else {
- diffs := deep.Equal(&expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
-}
-
-func TestStore_GetID_SetID(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- expected := v4.ID{
- BuildTimestamp: time.Now().UTC(),
- SchemaVersion: 2,
- }
-
- if err = s.SetID(expected); err != nil {
- t.Fatalf("failed to set ID: %+v", err)
- }
-
- assertIDReader(t, s, expected)
-
-}
-
-func assertVulnerabilityReader(t *testing.T, reader v4.VulnerabilityStoreReader, namespace, name string, expected []v4.Vulnerability) {
- if actual, err := reader.GetVulnerability(namespace, name); err != nil {
- t.Fatalf("failed to get Vulnerability: %+v", err)
- } else {
- if len(actual) != len(expected) {
- t.Fatalf("unexpected number of vulns: %d", len(actual))
- }
- for idx := range actual {
- diffs := deep.Equal(expected[idx], actual[idx])
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
- }
-}
-
-func TestStore_GetVulnerability_SetVulnerability(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- extra := []v4.Vulnerability{
- {
- ID: "my-cve-33333",
- PackageName: "package-name-2",
- Namespace: "my-namespace",
- VersionConstraint: "< 1.0",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- RelatedVulnerabilities: []v4.VulnerabilityReference{
- {
- ID: "another-cve",
- Namespace: "nvd",
- },
- {
- ID: "an-other-cve",
- Namespace: "nvd",
- },
- },
- Fix: v4.Fix{
- Versions: []string{"2.0.1"},
- State: v4.FixedState,
- },
- },
- {
- ID: "my-other-cve-33333",
- PackageName: "package-name-3",
- Namespace: "my-namespace",
- VersionConstraint: "< 509.2.2",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- RelatedVulnerabilities: []v4.VulnerabilityReference{
- {
- ID: "another-cve",
- Namespace: "nvd",
- },
- {
- ID: "an-other-cve",
- Namespace: "nvd",
- },
- },
- Fix: v4.Fix{
- State: v4.NotFixedState,
- },
- },
- }
-
- expected := []v4.Vulnerability{
- {
- ID: "my-cve",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 1.0",
- VersionFormat: "semver",
- CPEs: []string{"a-cool-cpe"},
- RelatedVulnerabilities: []v4.VulnerabilityReference{
- {
- ID: "another-cve",
- Namespace: "nvd",
- },
- {
- ID: "an-other-cve",
- Namespace: "nvd",
- },
- },
- Fix: v4.Fix{
- Versions: []string{"1.0.1"},
- State: v4.FixedState,
- },
- },
- {
- ID: "my-other-cve",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 509.2.2",
- VersionFormat: "semver",
- CPEs: nil,
- RelatedVulnerabilities: []v4.VulnerabilityReference{
- {
- ID: "another-cve",
- Namespace: "nvd",
- },
- {
- ID: "an-other-cve",
- Namespace: "nvd",
- },
- },
- Fix: v4.Fix{
- Versions: []string{"4.0.5"},
- State: v4.FixedState,
- },
- },
- {
- ID: "yet-another-cve",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 1000.0.0",
- VersionFormat: "semver",
- CPEs: nil,
- RelatedVulnerabilities: nil,
- Fix: v4.Fix{
- Versions: []string{"1000.0.1"},
- State: v4.FixedState,
- },
- },
- {
- ID: "yet-another-cve-with-advisories",
- PackageName: "package-name",
- Namespace: "my-namespace",
- VersionConstraint: "< 1000.0.0",
- VersionFormat: "semver",
- CPEs: nil,
- RelatedVulnerabilities: nil,
- Fix: v4.Fix{
- Versions: []string{"1000.0.1"},
- State: v4.FixedState,
- },
- Advisories: []v4.Advisory{{ID: "ABC-12345", Link: "https://abc.xyz"}},
- },
- }
-
- total := append(expected, extra...)
-
- if err = s.AddVulnerability(total...); err != nil {
- t.Fatalf("failed to set Vulnerability: %+v", err)
- }
-
- var allEntries []model.VulnerabilityModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != len(total) {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- assertVulnerabilityReader(t, s, expected[0].Namespace, expected[0].PackageName, expected)
-
-}
-
-func assertVulnerabilityMetadataReader(t *testing.T, reader v4.VulnerabilityMetadataStoreReader, id, namespace string, expected v4.VulnerabilityMetadata) {
- if actual, err := reader.GetVulnerabilityMetadata(id, namespace); err != nil {
- t.Fatalf("failed to get metadata: %+v", err)
- } else if actual == nil {
- t.Fatalf("no metadata returned for id=%q namespace=%q", id, namespace)
- } else {
- sortMetadataCvss(actual.Cvss)
- sortMetadataCvss(expected.Cvss)
-
- // make sure they both have the same number of CVSS entries - preventing a panic on later assertions
- assert.Len(t, expected.Cvss, len(actual.Cvss))
- for idx, actualCvss := range actual.Cvss {
- assert.Equal(t, actualCvss.Vector, expected.Cvss[idx].Vector)
- assert.Equal(t, actualCvss.Version, expected.Cvss[idx].Version)
- assert.Equal(t, actualCvss.Metrics, expected.Cvss[idx].Metrics)
-
- actualVendor, err := json.Marshal(actualCvss.VendorMetadata)
- if err != nil {
- t.Errorf("unable to marshal vendor metadata: %q", err)
- }
- expectedVendor, err := json.Marshal(expected.Cvss[idx].VendorMetadata)
- if err != nil {
- t.Errorf("unable to marshal vendor metadata: %q", err)
- }
- assert.Equal(t, string(actualVendor), string(expectedVendor))
-
- }
-
- // nil the Cvss field because it is an interface - verification of Cvss
- // has already happened at this point
- expected.Cvss = nil
- actual.Cvss = nil
- assert.Equal(t, &expected, actual)
- }
-
-}
-
-func sortMetadataCvss(cvss []v4.Cvss) {
- sort.Slice(cvss, func(i, j int) bool {
- // first, sort by Vector
- if cvss[i].Vector > cvss[j].Vector {
- return true
- }
- if cvss[i].Vector < cvss[j].Vector {
- return false
- }
- // then try to sort by BaseScore if Vector is the same
- return cvss[i].Metrics.BaseScore < cvss[j].Metrics.BaseScore
- })
-}
-
-// CustomMetadata is effectively a noop, its values aren't meaningful and are
-// mostly useful to ensure that any type can be stored and then retrieved for
-// assertion in these test cases where custom vendor CVSS scores are used
-type CustomMetadata struct {
- SuperScore string
- Vendor string
-}
-
-func TestStore_GetVulnerabilityMetadata_SetVulnerabilityMetadata(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- total := []v4.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v4.Cvss{
- {
- VendorMetadata: CustomMetadata{
- Vendor: "redhat",
- SuperScore: "1000",
- },
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 1.1,
- 2.2,
- 3.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NOT",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.3,
- 2.1,
- 3.2,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NICE",
- VendorMetadata: nil,
- },
- },
- },
- {
- ID: "my-other-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- }
-
- if err = s.AddVulnerabilityMetadata(total...); err != nil {
- t.Fatalf("failed to set metadata: %+v", err)
- }
-
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != len(total) {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
-}
-
-func TestStore_MergeVulnerabilityMetadata(t *testing.T) {
- tests := []struct {
- name string
- add []v4.VulnerabilityMetadata
- expected v4.VulnerabilityMetadata
- err bool
- }{
- {
- name: "go-case",
- add: []v4.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v4.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- {
- name: "merge-links",
- add: []v4.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://google.com"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://yahoo.com"},
- },
- },
- expected: v4.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re", "https://google.com", "https://yahoo.com"},
- Cvss: []v4.Cvss{},
- },
- },
- {
- name: "bad-severity",
- add: []v4.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "meh, push that for next tuesday...",
- URLs: []string{"https://redhat.com"},
- },
- },
- err: true,
- },
- {
- name: "mismatch-description",
- err: true,
- add: []v4.VulnerabilityMetadata{
- {
-
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- },
- {
- name: "mismatch-cvss2",
- err: false,
- add: []v4.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v4.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:P--VERY",
- },
- },
- },
- },
- {
- name: "mismatch-cvss3",
- err: false,
- add: []v4.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 0,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v4.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "best description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 0,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- dbTempDir := t.TempDir()
- s, err := New(dbTempDir, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- // add each metadata in order
- var theErr error
- for _, metadata := range test.add {
- err = s.AddVulnerabilityMetadata(metadata)
- if err != nil {
- theErr = err
- break
- }
- }
-
- if test.err && theErr == nil {
- t.Fatalf("expected error but did not get one")
- } else if !test.err && theErr != nil {
- t.Fatalf("expected no error but got one: %+v", theErr)
- } else if test.err && theErr != nil {
- // test pass...
- return
- }
-
- // ensure there is exactly one entry
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != 1 {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- // get the resulting metadata object
- if actual, err := s.GetVulnerabilityMetadata(test.expected.ID, test.expected.Namespace); err != nil {
- t.Fatalf("failed to get metadata: %+v", err)
- } else {
- diffs := deep.Equal(&test.expected, actual)
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
- })
- }
-}
-
-func TestCvssScoresInMetadata(t *testing.T) {
- tests := []struct {
- name string
- add []v4.VulnerabilityMetadata
- expected v4.VulnerabilityMetadata
- }{
- {
- name: "append-cvss",
- add: []v4.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v4.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- {
- name: "append-vendor-cvss",
- add: []v4.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- VendorMetadata: CustomMetadata{
- SuperScore: "100",
- Vendor: "debian",
- },
- },
- },
- },
- },
- expected: v4.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- },
- {
- Version: "2.0",
- Metrics: v4.NewCvssMetrics(
- 4.1,
- 5.2,
- 6.3,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
- VendorMetadata: CustomMetadata{
- SuperScore: "100",
- Vendor: "debian",
- },
- },
- },
- },
- },
- {
- name: "avoids-duplicate-cvss",
- add: []v4.VulnerabilityMetadata{
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- {
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- expected: v4.VulnerabilityMetadata{
- ID: "my-cve",
- RecordSource: "record-source",
- Namespace: "namespace",
- Severity: "pretty bad",
- URLs: []string{"https://ancho.re"},
- Description: "worst description ever",
- Cvss: []v4.Cvss{
- {
- Version: "3.0",
- Metrics: v4.NewCvssMetrics(
- 1.4,
- 2.5,
- 3.6,
- ),
- Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
- },
- },
- },
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- dbTempDir := t.TempDir()
-
- s, err := New(dbTempDir, true)
- if err != nil {
- t.Fatalf("could not create s: %+v", err)
- }
-
- // add each metadata in order
- for _, metadata := range test.add {
- err = s.AddVulnerabilityMetadata(metadata)
- if err != nil {
- t.Fatalf("unable to s vulnerability metadata: %+v", err)
- }
- }
-
- // ensure there is exactly one entry
- var allEntries []model.VulnerabilityMetadataModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != 1 {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
-
- assertVulnerabilityMetadataReader(t, s, test.expected.ID, test.expected.Namespace, test.expected)
- })
- }
-}
-
-func assertVulnerabilityMatchExclusionReader(t *testing.T, reader v4.VulnerabilityMatchExclusionStoreReader, id string, expected []v4.VulnerabilityMatchExclusion) {
- if actual, err := reader.GetVulnerabilityMatchExclusion(id); err != nil {
- t.Fatalf("failed to get Vulnerability Match Exclusion: %+v", err)
- } else {
- t.Logf("%+v", actual)
- if len(actual) != len(expected) {
- t.Fatalf("unexpected number of vulnerability match exclusions: expected=%d, actual=%d", len(expected), len(actual))
- }
- for idx := range actual {
- diffs := deep.Equal(expected[idx], actual[idx])
- if len(diffs) > 0 {
- for _, d := range diffs {
- t.Errorf("Diff: %+v", d)
- }
- }
- }
- }
-}
-
-func TestStore_GetVulnerabilityMatchExclusion_SetVulnerabilityMatchExclusion(t *testing.T) {
- dbTempFile := t.TempDir()
-
- s, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- extra := []v4.VulnerabilityMatchExclusion{
- {
- ID: "CVE-1234-14567",
- Constraints: []v4.VulnerabilityMatchExclusionConstraint{
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "extra-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Name: "abc",
- Language: "ruby",
- Version: "1.2.3",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "extra-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Name: "abc",
- Language: "ruby",
- Version: "4.5.6",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "extra-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Name: "time-1",
- Language: "ruby",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "extra-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Name: "abc.xyz:nothing-of-interest",
- Type: "java-archive",
- },
- },
- },
- Justification: "Because I said so.",
- },
- {
- ID: "CVE-1234-10",
- Constraints: nil,
- Justification: "Because I said so.",
- },
- }
-
- expected := []v4.VulnerabilityMatchExclusion{
- {
- ID: "CVE-1234-9999999",
- Constraints: []v4.VulnerabilityMatchExclusionConstraint{
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "old-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Language: "python",
- Name: "abc",
- Version: "1.2.3",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "old-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Language: "python",
- Name: "abc",
- Version: "4.5.6",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "old-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Language: "python",
- Name: "time-245",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "old-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Type: "npm",
- Name: "everything",
- },
- },
- },
- Justification: "This is a false positive",
- },
- {
- ID: "CVE-1234-9999999",
- Constraints: []v4.VulnerabilityMatchExclusionConstraint{
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "old-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Language: "go",
- Type: "go-module",
- Name: "abc",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- Namespace: "some-other-namespace:cpe",
- },
- Package: v4.PackageExclusionConstraint{
- Language: "go",
- Type: "go-module",
- Name: "abc",
- },
- },
- {
- Vulnerability: v4.VulnerabilityExclusionConstraint{
- FixState: "wont-fix",
- },
- },
- },
- Justification: "This is also a false positive",
- },
- {
- ID: "CVE-1234-9999999",
- Justification: "global exclude",
- },
- }
-
- total := append(expected, extra...)
-
- if err = s.AddVulnerabilityMatchExclusion(total...); err != nil {
- t.Fatalf("failed to set Vulnerability Match Exclusion: %+v", err)
- }
-
- var allEntries []model.VulnerabilityMatchExclusionModel
- s.(*store).db.Find(&allEntries)
- if len(allEntries) != len(total) {
- t.Fatalf("unexpected number of entries: %d", len(allEntries))
- }
- assertVulnerabilityMatchExclusionReader(t, s, expected[0].ID, expected)
-}
-
-func Test_DiffStore(t *testing.T) {
- //GIVEN
- dbTempFile := t.TempDir()
-
- s1, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
- dbTempFile = t.TempDir()
-
- s2, err := New(dbTempFile, true)
- if err != nil {
- t.Fatalf("could not create store: %+v", err)
- }
-
- baseVulns := []v4.Vulnerability{
- {
- Namespace: "github:language:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 2.0 >= 1.29",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "github:language:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- PackageName: "npm:axios",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"},
- Fix: v4.Fix{
- State: v4.UnknownFixState,
- },
- },
- {
- Namespace: "nuget",
- ID: "GHSA-****-******",
- PackageName: "nuget:net",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:nuget:net:*:*:*:*:*:*"},
- Fix: v4.Fix{
- State: v4.UnknownFixState,
- },
- },
- {
- Namespace: "hex",
- ID: "GHSA-^^^^-^^^^^^",
- PackageName: "hex:esbuild",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:hex:esbuild:*:*:*:*:*:*"},
- },
- }
- baseMetadata := []v4.VulnerabilityMetadata{
- {
- Namespace: "nuget",
- ID: "GHSA-****-******",
- DataSource: "nvd",
- },
- }
- targetVulns := []v4.Vulnerability{
- {
- Namespace: "github:language:python",
- ID: "CVE-123-4567",
- PackageName: "pypi:requests",
- VersionConstraint: "< 2.0 >= 1.29",
- CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"},
- },
- {
- Namespace: "github:language:go",
- ID: "GHSA-....-....",
- PackageName: "hashicorp:nomad",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:golang:hashicorp:nomad:*:*:*:*:*"},
- },
- {
- Namespace: "github:language:go",
- ID: "GHSA-....-....",
- PackageName: "hashicorp:n",
- VersionConstraint: "< 2.0 >= 1.17",
- CPEs: []string{"cpe:2.3:golang:hashicorp:n:*:*:*:*:*"},
- },
- {
- Namespace: "npm",
- ID: "CVE-123-7654",
- PackageName: "npm:axios",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"},
- Fix: v4.Fix{
- State: v4.WontFixState,
- },
- },
- {
- Namespace: "nuget",
- ID: "GHSA-****-******",
- PackageName: "nuget:net",
- VersionConstraint: "< 3.0 >= 2.17",
- CPEs: []string{"cpe:2.3:nuget:net:*:*:*:*:*:*"},
- Fix: v4.Fix{
- State: v4.UnknownFixState,
- },
- },
- }
- expectedDiffs := []v4.Diff{
- {
- Reason: v4.DiffChanged,
- ID: "CVE-123-4567",
- Namespace: "github:language:python",
- Packages: []string{"pypi:requests"},
- },
- {
- Reason: v4.DiffChanged,
- ID: "CVE-123-7654",
- Namespace: "npm",
- Packages: []string{"npm:axios"},
- },
- {
- Reason: v4.DiffRemoved,
- ID: "GHSA-****-******",
- Namespace: "nuget",
- Packages: []string{"nuget:net"},
- },
- {
- Reason: v4.DiffAdded,
- ID: "GHSA-....-....",
- Namespace: "github:language:go",
- Packages: []string{"hashicorp:n", "hashicorp:nomad"},
- },
- {
- Reason: v4.DiffRemoved,
- ID: "GHSA-^^^^-^^^^^^",
- Namespace: "hex",
- Packages: []string{"hex:esbuild"},
- },
- }
-
- for _, vuln := range baseVulns {
- s1.AddVulnerability(vuln)
- }
- for _, vuln := range targetVulns {
- s2.AddVulnerability(vuln)
- }
- for _, meta := range baseMetadata {
- s1.AddVulnerabilityMetadata(meta)
- }
-
- //WHEN
- result, err := s1.DiffStore(s2)
-
- //THEN
- sort.SliceStable(*result, func(i, j int) bool {
- return (*result)[i].ID < (*result)[j].ID
- })
- for i := range *result {
- sort.Strings((*result)[i].Packages)
- }
-
- assert.NoError(t, err)
- assert.Equal(t, expectedDiffs, *result)
-}
diff --git a/grype/db/v4/vulnerability.go b/grype/db/v4/vulnerability.go
deleted file mode 100644
index 2104b4a7df9..00000000000
--- a/grype/db/v4/vulnerability.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package v4
-
-import (
- "sort"
- "strings"
-)
-
-// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.
-type Vulnerability struct {
- ID string `json:"id"` // The identifier of the vulnerability or advisory
- PackageName string `json:"package_name"` // The name of the package that is vulnerable
- Namespace string `json:"namespace"` // The ecosystem where the package resides
- VersionConstraint string `json:"version_constraint"` // The version range which the given package is vulnerable
- VersionFormat string `json:"version_format"` // The format which all version fields should be interpreted as
- CPEs []string `json:"cpes"` // The CPEs which are considered vulnerable
- RelatedVulnerabilities []VulnerabilityReference `json:"related_vulnerabilities"` // Other Vulnerabilities that are related to this one (e.g. GHSA relate to CVEs, or how distro CVE relates to NVD record)
- Fix Fix `json:"fix"` // All information about fixed versions
- Advisories []Advisory `json:"advisories"` // Any vendor advisories about fixes or other notifications about this vulnerability
-}
-
-type VulnerabilityReference struct {
- ID string `json:"id"`
- Namespace string `json:"namespace"`
-}
-
-//nolint:gocognit
-func (v *Vulnerability) Equal(vv Vulnerability) bool {
- equal := v.ID == vv.ID &&
- v.PackageName == vv.PackageName &&
- v.Namespace == vv.Namespace &&
- v.VersionConstraint == vv.VersionConstraint &&
- v.VersionFormat == vv.VersionFormat &&
- len(v.CPEs) == len(vv.CPEs) &&
- len(v.RelatedVulnerabilities) == len(vv.RelatedVulnerabilities) &&
- len(v.Advisories) == len(vv.Advisories) &&
- v.Fix.State == vv.Fix.State &&
- len(v.Fix.Versions) == len(vv.Fix.Versions)
-
- if !equal {
- return false
- }
-
- sort.Strings(v.CPEs)
- sort.Strings(vv.CPEs)
- for idx, cpe := range v.CPEs {
- if cpe != vv.CPEs[idx] {
- return false
- }
- }
-
- sortedBaseRelVulns, sortedTargetRelVulns := sortRelatedVulns(v.RelatedVulnerabilities), sortRelatedVulns(vv.RelatedVulnerabilities)
- for idx, item := range sortedBaseRelVulns {
- if item != sortedTargetRelVulns[idx] {
- return false
- }
- }
- sortedBaseAdvisories, sortedTargetAdvisories := sortAdvisories(v.Advisories), sortAdvisories(vv.Advisories)
- for idx, item := range sortedBaseAdvisories {
- if item != sortedTargetAdvisories[idx] {
- return false
- }
- }
- sort.Strings(v.Fix.Versions)
- sort.Strings(vv.Fix.Versions)
- for idx, item := range v.Fix.Versions {
- if item != vv.Fix.Versions[idx] {
- return false
- }
- }
-
- return true
-}
-
-func sortRelatedVulns(vulns []VulnerabilityReference) []VulnerabilityReference {
- sort.SliceStable(vulns, func(i, j int) bool {
- b1, b2 := strings.Builder{}, strings.Builder{}
- b1.WriteString(vulns[i].ID)
- b1.WriteString(vulns[i].Namespace)
- b2.WriteString(vulns[j].ID)
- b2.WriteString(vulns[j].Namespace)
- return b1.String() < b2.String()
- })
- return vulns
-}
-
-func sortAdvisories(advisories []Advisory) []Advisory {
- sort.SliceStable(advisories, func(i, j int) bool {
- b1, b2 := strings.Builder{}, strings.Builder{}
- b1.WriteString(advisories[i].ID)
- b1.WriteString(advisories[i].Link)
- b2.WriteString(advisories[j].ID)
- b2.WriteString(advisories[j].Link)
- return b1.String() < b2.String()
- })
- return advisories
-}
diff --git a/grype/db/v4/vulnerability_match_exclusion.go b/grype/db/v4/vulnerability_match_exclusion.go
deleted file mode 100644
index 6546c00a04b..00000000000
--- a/grype/db/v4/vulnerability_match_exclusion.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package v4
-
-import (
- "encoding/json"
-)
-
-// VulnerabilityMatchExclusion represents the minimum data fields necessary to automatically filter certain
-// vulnerabilities from match results based on the specified constraints.
-type VulnerabilityMatchExclusion struct {
- ID string `json:"id"` // The identifier of the vulnerability or advisory
- Constraints []VulnerabilityMatchExclusionConstraint `json:"constraints,omitempty"` // The constraints under which the exclusion applies
- Justification string `json:"justification"` // Justification for the exclusion
-}
-
-// VulnerabilityMatchExclusionConstraint describes criteria for which matches should be excluded
-type VulnerabilityMatchExclusionConstraint struct {
- Vulnerability VulnerabilityExclusionConstraint `json:"vulnerability,omitempty"` // Vulnerability exclusion criteria
- Package PackageExclusionConstraint `json:"package,omitempty"` // Package exclusion criteria
- ExtraFields map[string]interface{} `json:"-"`
-}
-
-func (c VulnerabilityMatchExclusionConstraint) Usable() bool {
- return len(c.ExtraFields) == 0 && c.Vulnerability.Usable() && c.Package.Usable()
-}
-
-func (c *VulnerabilityMatchExclusionConstraint) UnmarshalJSON(data []byte) error {
- // Create a new type from the target type to avoid recursion.
- type _vulnerabilityMatchExclusionConstraint VulnerabilityMatchExclusionConstraint
-
- // Unmarshal into an instance of the new type.
- var _c _vulnerabilityMatchExclusionConstraint
- if err := json.Unmarshal(data, &_c); err != nil {
- return err
- }
-
- if err := json.Unmarshal(data, &_c.ExtraFields); err != nil {
- return err
- }
-
- delete(_c.ExtraFields, "vulnerability")
- delete(_c.ExtraFields, "package")
-
- if len(_c.ExtraFields) == 0 {
- _c.ExtraFields = nil
- }
-
- // Cast the new type instance to the original type and assign.
- *c = VulnerabilityMatchExclusionConstraint(_c)
- return nil
-}
-
-// VulnerabilityExclusionConstraint describes criteria for excluding a match based on additional vulnerability components
-type VulnerabilityExclusionConstraint struct {
- Namespace string `json:"namespace,omitempty"` // Vulnerability namespace
- FixState FixState `json:"fix_state,omitempty"` // Vulnerability fix state
- ExtraFields map[string]interface{} `json:"-"`
-}
-
-func (v VulnerabilityExclusionConstraint) Usable() bool {
- return len(v.ExtraFields) == 0
-}
-
-func (v *VulnerabilityExclusionConstraint) UnmarshalJSON(data []byte) error {
- // Create a new type from the target type to avoid recursion.
- type _vulnerabilityExclusionConstraint VulnerabilityExclusionConstraint
-
- // Unmarshal into an instance of the new type.
- var _v _vulnerabilityExclusionConstraint
- if err := json.Unmarshal(data, &_v); err != nil {
- return err
- }
-
- if err := json.Unmarshal(data, &_v.ExtraFields); err != nil {
- return err
- }
-
- delete(_v.ExtraFields, "namespace")
- delete(_v.ExtraFields, "fix_state")
-
- if len(_v.ExtraFields) == 0 {
- _v.ExtraFields = nil
- }
-
- // Cast the new type instance to the original type and assign.
- *v = VulnerabilityExclusionConstraint(_v)
- return nil
-}
-
-// PackageExclusionConstraint describes criteria for excluding a match based on package components
-type PackageExclusionConstraint struct {
- Name string `json:"name,omitempty"` // Package name
- Language string `json:"language,omitempty"` // The language ecosystem for a package
- Type string `json:"type,omitempty"` // Package type
- Version string `json:"version,omitempty"` // Package version
- Location string `json:"location,omitempty"` // Package location
- ExtraFields map[string]interface{} `json:"-"`
-}
-
-func (p PackageExclusionConstraint) Usable() bool {
- return len(p.ExtraFields) == 0
-}
-
-func (p *PackageExclusionConstraint) UnmarshalJSON(data []byte) error {
- // Create a new type from the target type to avoid recursion.
- type _packageExclusionConstraint PackageExclusionConstraint
-
- // Unmarshal into an instance of the new type.
- var _p _packageExclusionConstraint
- if err := json.Unmarshal(data, &_p); err != nil {
- return err
- }
-
- if err := json.Unmarshal(data, &_p.ExtraFields); err != nil {
- return err
- }
-
- delete(_p.ExtraFields, "name")
- delete(_p.ExtraFields, "language")
- delete(_p.ExtraFields, "type")
- delete(_p.ExtraFields, "version")
- delete(_p.ExtraFields, "location")
-
- if len(_p.ExtraFields) == 0 {
- _p.ExtraFields = nil
- }
-
- // Cast the new type instance to the original type and assign.
- *p = PackageExclusionConstraint(_p)
- return nil
-}
diff --git a/grype/db/v4/vulnerability_match_exclusion_store.go b/grype/db/v4/vulnerability_match_exclusion_store.go
deleted file mode 100644
index f2b3ebb57a6..00000000000
--- a/grype/db/v4/vulnerability_match_exclusion_store.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package v4
-
-type VulnerabilityMatchExclusionStore interface {
- VulnerabilityMatchExclusionStoreReader
- VulnerabilityMatchExclusionStoreWriter
-}
-
-type VulnerabilityMatchExclusionStoreReader interface {
- GetVulnerabilityMatchExclusion(id string) ([]VulnerabilityMatchExclusion, error)
-}
-
-type VulnerabilityMatchExclusionStoreWriter interface {
- AddVulnerabilityMatchExclusion(exclusion ...VulnerabilityMatchExclusion) error
-}
diff --git a/grype/db/v4/vulnerability_metadata.go b/grype/db/v4/vulnerability_metadata.go
deleted file mode 100644
index c13f909016d..00000000000
--- a/grype/db/v4/vulnerability_metadata.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package v4
-
-import "reflect"
-
-// VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching.
-type VulnerabilityMetadata struct {
- ID string `json:"id"` // The identifier of the vulnerability or advisory
- Namespace string `json:"namespace"` // Where this entry is valid within
- DataSource string `json:"data_source"` // A URL where the data was sourced from
- RecordSource string `json:"record_source"` // The source of the vulnerability information (relative to the immediate upstream in the enterprise feedgroup)
- Severity string `json:"severity"` // How severe the vulnerability is (valid values are defined by upstream sources currently)
- URLs []string `json:"urls"` // URLs to get more information about the vulnerability or advisory
- Description string `json:"description"` // Description of the vulnerability
- Cvss []Cvss `json:"cvss"` // Common Vulnerability Scoring System values
-}
-
-// Cvss contains select Common Vulnerability Scoring System fields for a vulnerability.
-type Cvss struct {
- // VendorMetadata captures non-standard CVSS fields that vendors can sometimes
- // include when providing CVSS information. This vendor-specific metadata type
- // allows to capture that data for persisting into the database
- VendorMetadata interface{} `json:"vendor_metadata"`
- Metrics CvssMetrics `json:"metrics"`
- Vector string `json:"vector"` // A textual representation of the metric values used to determine the score
- Version string `json:"version"` // The version of the CVSS spec, for example 2.0, 3.0, or 3.1
-}
-
-// CvssMetrics are the quantitative values that make up a CVSS score.
-type CvssMetrics struct {
- // BaseScore ranges from 0 - 10 and defines qualities intrinsic to the severity of a vulnerability.
- BaseScore float64 `json:"base_score"`
- // ExploitabilityScore is a pointer to avoid having a 0 value by default.
- // It is an indicator of how easy it may be for an attacker to exploit
- // a vulnerability
- ExploitabilityScore *float64 `json:"exploitability_score"`
- // ImpactScore represents the effects of an exploited vulnerability
- // relative to compromise in confidentiality, integrity, and availability.
- // It is an optional parameter, so that is why it is a pointer instead of
- // a regular field
- ImpactScore *float64 `json:"impact_score"`
-}
-
-func NewCvssMetrics(baseScore, exploitabilityScore, impactScore float64) CvssMetrics {
- return CvssMetrics{
- BaseScore: baseScore,
- ExploitabilityScore: &exploitabilityScore,
- ImpactScore: &impactScore,
- }
-}
-
-func (v *VulnerabilityMetadata) Equal(vv VulnerabilityMetadata) bool {
- equal := v.ID == vv.ID &&
- v.Namespace == vv.Namespace &&
- v.DataSource == vv.DataSource &&
- v.RecordSource == vv.RecordSource &&
- v.Severity == vv.Severity &&
- v.Description == vv.Description &&
- len(v.URLs) == len(vv.URLs) &&
- len(v.Cvss) == len(vv.Cvss)
-
- if !equal {
- return false
- }
- for idx, cpe := range v.URLs {
- if cpe != vv.URLs[idx] {
- return false
- }
- }
- for idx, item := range v.Cvss {
- if !reflect.DeepEqual(item, vv.Cvss[idx]) {
- return false
- }
- }
-
- return true
-}
diff --git a/grype/db/v4/vulnerability_metadata_store.go b/grype/db/v4/vulnerability_metadata_store.go
deleted file mode 100644
index 9f99c46efcd..00000000000
--- a/grype/db/v4/vulnerability_metadata_store.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package v4
-
-type VulnerabilityMetadataStore interface {
- VulnerabilityMetadataStoreReader
- VulnerabilityMetadataStoreWriter
-}
-
-type VulnerabilityMetadataStoreReader interface {
- GetVulnerabilityMetadata(id, namespace string) (*VulnerabilityMetadata, error)
- GetAllVulnerabilityMetadata() (*[]VulnerabilityMetadata, error)
-}
-
-type VulnerabilityMetadataStoreWriter interface {
- AddVulnerabilityMetadata(metadata ...VulnerabilityMetadata) error
-}
diff --git a/grype/db/v4/vulnerability_store.go b/grype/db/v4/vulnerability_store.go
deleted file mode 100644
index 84dd3c9fac1..00000000000
--- a/grype/db/v4/vulnerability_store.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package v4
-
-const VulnerabilityStoreFileName = "vulnerability.db"
-
-type VulnerabilityStore interface {
- VulnerabilityStoreReader
- VulnerabilityStoreWriter
-}
-
-type VulnerabilityStoreReader interface {
- // GetVulnerabilityNamespaces retrieves unique list of vulnerability namespaces
- GetVulnerabilityNamespaces() ([]string, error)
- // GetVulnerability retrieves vulnerabilities by namespace and package
- GetVulnerability(namespace, packageName string) ([]Vulnerability, error)
- GetAllVulnerabilities() (*[]Vulnerability, error)
-}
-
-type VulnerabilityStoreWriter interface {
- // AddVulnerability inserts a new record of a vulnerability into the store
- AddVulnerability(vulnerabilities ...Vulnerability) error
-}
diff --git a/grype/db/v5/advisory.go b/grype/db/v5/advisory.go
index f94176db33e..7a856338d1d 100644
--- a/grype/db/v5/advisory.go
+++ b/grype/db/v5/advisory.go
@@ -1,6 +1,6 @@
package v5
-// Advisory represents published statements regarding a vulnerability (and potentially about it's resolution).
+// Advisory represents published statements regarding a vulnerability (and potentially about its resolution).
type Advisory struct {
ID string `json:"id"`
Link string `json:"link"`
diff --git a/grype/db/v5/cvss.go b/grype/db/v5/cvss.go
new file mode 100644
index 00000000000..6ee6965e66a
--- /dev/null
+++ b/grype/db/v5/cvss.go
@@ -0,0 +1,25 @@
+package v5
+
+import (
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+func NewCvss(m []Cvss) []vulnerability.Cvss {
+ //nolint:prealloc
+ var cvss []vulnerability.Cvss
+ for _, score := range m {
+ cvss = append(cvss, vulnerability.Cvss{
+ Source: score.Source,
+ Type: score.Type,
+ Version: score.Version,
+ Vector: score.Vector,
+ Metrics: vulnerability.CvssMetrics{
+ BaseScore: score.Metrics.BaseScore,
+ ExploitabilityScore: score.Metrics.ExploitabilityScore,
+ ImpactScore: score.Metrics.ImpactScore,
+ },
+ VendorMetadata: score.VendorMetadata,
+ })
+ }
+ return cvss
+}
diff --git a/grype/differ/differ.go b/grype/db/v5/differ/differ.go
similarity index 79%
rename from grype/differ/differ.go
rename to grype/db/v5/differ/differ.go
index a74585307a2..5054a46a653 100644
--- a/grype/differ/differ.go
+++ b/grype/db/v5/differ/differ.go
@@ -11,19 +11,20 @@ import (
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
- "github.com/anchore/grype/grype/db"
v5 "github.com/anchore/grype/grype/db/v5"
+ legacyDistribution "github.com/anchore/grype/grype/db/v5/distribution"
"github.com/anchore/grype/grype/event"
"github.com/anchore/grype/internal/bus"
+ "github.com/anchore/grype/internal/log"
)
type Differ struct {
- baseCurator db.Curator
- targetCurator db.Curator
+ baseCurator legacyDistribution.Curator
+ targetCurator legacyDistribution.Curator
}
-func NewDiffer(config db.Config) (*Differ, error) {
- baseCurator, err := db.NewCurator(db.Config{
+func NewDiffer(config legacyDistribution.Config) (*Differ, error) {
+ baseCurator, err := legacyDistribution.NewCurator(legacyDistribution.Config{
DBRootDir: path.Join(config.DBRootDir, "diff", "base"),
ListingURL: config.ListingURL,
CACert: config.CACert,
@@ -33,7 +34,7 @@ func NewDiffer(config db.Config) (*Differ, error) {
return nil, err
}
- targetCurator, err := db.NewCurator(db.Config{
+ targetCurator, err := legacyDistribution.NewCurator(legacyDistribution.Config{
DBRootDir: path.Join(config.DBRootDir, "diff", "target"),
ListingURL: config.ListingURL,
CACert: config.CACert,
@@ -57,11 +58,11 @@ func (d *Differ) SetTargetDB(target string) error {
return d.setOrDownload(&d.targetCurator, target)
}
-func (d *Differ) setOrDownload(curator *db.Curator, filenameOrURL string) error {
+func (d *Differ) setOrDownload(curator *legacyDistribution.Curator, filenameOrURL string) error {
u, err := url.ParseRequestURI(filenameOrURL)
if err != nil || u.Scheme == "" {
- *curator, err = db.NewCurator(db.Config{
+ *curator, err = legacyDistribution.NewCurator(legacyDistribution.Config{
DBRootDir: filenameOrURL,
})
if err != nil {
@@ -76,7 +77,7 @@ func (d *Differ) setOrDownload(curator *db.Curator, filenameOrURL string) error
available := listings.Available
dbs := available[v5.SchemaVersion]
- var listing *db.ListingEntry
+ var listing *legacyDistribution.ListingEntry
for _, d := range dbs {
database := d
@@ -97,12 +98,10 @@ func (d *Differ) setOrDownload(curator *db.Curator, filenameOrURL string) error
return nil
}
-func download(curator *db.Curator, listing *db.ListingEntry) error {
+func download(curator *legacyDistribution.Curator, listing *legacyDistribution.ListingEntry) error {
// let consumers know of a monitorable event (download + import stages)
importProgress := progress.NewManual(1)
- stage := &progress.Stage{
- Current: "checking available databases",
- }
+ stage := progress.NewAtomicStage("checking available databases")
downloadProgress := progress.NewManual(1)
aggregateProgress := progress.NewAggregator(progress.DefaultStrategy, downloadProgress, importProgress)
@@ -124,19 +123,19 @@ func download(curator *db.Curator, listing *db.ListingEntry) error {
}
func (d *Differ) DiffDatabases() (*[]v5.Diff, error) {
- baseStore, baseDBCloser, err := d.baseCurator.GetStore()
+ baseStore, err := d.baseCurator.GetStore()
if err != nil {
return nil, err
}
- defer baseDBCloser.Close()
+ defer log.CloseAndLogError(baseStore, d.baseCurator.Status().Location)
- targetStore, targetDBCloser, err := d.targetCurator.GetStore()
+ targetStore, err := d.targetCurator.GetStore()
if err != nil {
return nil, err
}
- defer targetDBCloser.Close()
+ defer log.CloseAndLogError(targetStore, d.targetCurator.Status().Location)
return baseStore.DiffStore(targetStore)
}
diff --git a/grype/differ/differ_test.go b/grype/db/v5/differ/differ_test.go
similarity index 87%
rename from grype/differ/differ_test.go
rename to grype/db/v5/differ/differ_test.go
index 7aee002caaa..dd0094faaf4 100644
--- a/grype/differ/differ_test.go
+++ b/grype/db/v5/differ/differ_test.go
@@ -10,16 +10,15 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/go-testutils"
- "github.com/anchore/grype/grype/db"
v5 "github.com/anchore/grype/grype/db/v5"
- "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/db/v5/distribution"
)
var update = flag.Bool("update", false, "update the *.golden files for diff presenter")
func TestNewDiffer(t *testing.T) {
//GIVEN
- config := db.Config{}
+ config := distribution.Config{}
//WHEN
differ, err := NewDiffer(config)
@@ -30,7 +29,7 @@ func TestNewDiffer(t *testing.T) {
}
func Test_DifferDirectory(t *testing.T) {
- d, err := NewDiffer(db.Config{
+ d, err := NewDiffer(distribution.Config{
DBRootDir: "root-dir",
})
require.NoError(t, err)
@@ -39,13 +38,13 @@ func Test_DifferDirectory(t *testing.T) {
require.NoError(t, err)
baseStatus := d.baseCurator.Status()
- require.Equal(t, "test-fixtures/dbs/base/"+strconv.Itoa(vulnerability.SchemaVersion), baseStatus.Location)
+ require.Equal(t, "test-fixtures/dbs/base/"+strconv.Itoa(v5.SchemaVersion), baseStatus.Location)
err = d.SetTargetDB("test-fixtures/dbs/target")
require.NoError(t, err)
targetStatus := d.targetCurator.Status()
- require.Equal(t, "test-fixtures/dbs/target/"+strconv.Itoa(vulnerability.SchemaVersion), targetStatus.Location)
+ require.Equal(t, "test-fixtures/dbs/target/"+strconv.Itoa(v5.SchemaVersion), targetStatus.Location)
}
func TestPresent_Json(t *testing.T) {
diff --git a/grype/differ/test-fixtures/dbs/base/5/metadata.json b/grype/db/v5/differ/test-fixtures/dbs/base/5/metadata.json
similarity index 100%
rename from grype/differ/test-fixtures/dbs/base/5/metadata.json
rename to grype/db/v5/differ/test-fixtures/dbs/base/5/metadata.json
diff --git a/grype/differ/test-fixtures/dbs/base/5/vulnerability.db b/grype/db/v5/differ/test-fixtures/dbs/base/5/vulnerability.db
similarity index 100%
rename from grype/differ/test-fixtures/dbs/base/5/vulnerability.db
rename to grype/db/v5/differ/test-fixtures/dbs/base/5/vulnerability.db
diff --git a/grype/differ/test-fixtures/dbs/target/5/metadata.json b/grype/db/v5/differ/test-fixtures/dbs/target/5/metadata.json
similarity index 100%
rename from grype/differ/test-fixtures/dbs/target/5/metadata.json
rename to grype/db/v5/differ/test-fixtures/dbs/target/5/metadata.json
diff --git a/grype/differ/test-fixtures/dbs/target/5/vulnerability.db b/grype/db/v5/differ/test-fixtures/dbs/target/5/vulnerability.db
similarity index 100%
rename from grype/differ/test-fixtures/dbs/target/5/vulnerability.db
rename to grype/db/v5/differ/test-fixtures/dbs/target/5/vulnerability.db
diff --git a/grype/differ/test-fixtures/snapshot/TestPresent_Json.golden b/grype/db/v5/differ/test-fixtures/snapshot/TestPresent_Json.golden
similarity index 100%
rename from grype/differ/test-fixtures/snapshot/TestPresent_Json.golden
rename to grype/db/v5/differ/test-fixtures/snapshot/TestPresent_Json.golden
diff --git a/grype/differ/test-fixtures/snapshot/TestPresent_Table.golden b/grype/db/v5/differ/test-fixtures/snapshot/TestPresent_Table.golden
similarity index 100%
rename from grype/differ/test-fixtures/snapshot/TestPresent_Table.golden
rename to grype/db/v5/differ/test-fixtures/snapshot/TestPresent_Table.golden
diff --git a/grype/db/curator.go b/grype/db/v5/distribution/curator.go
similarity index 61%
rename from grype/db/curator.go
rename to grype/db/v5/distribution/curator.go
index 57711aa0c4c..609d67a565b 100644
--- a/grype/db/curator.go
+++ b/grype/db/v5/distribution/curator.go
@@ -1,4 +1,4 @@
-package db
+package distribution
import (
"crypto/tls"
@@ -11,65 +11,84 @@ import (
"time"
"github.com/hako/durafmt"
- cleanhttp "github.com/hashicorp/go-cleanhttp"
- archiver "github.com/mholt/archiver/v3"
+ "github.com/hashicorp/go-cleanhttp"
"github.com/spf13/afero"
- partybus "github.com/wagoodman/go-partybus"
- progress "github.com/wagoodman/go-progress"
+ "github.com/wagoodman/go-partybus"
+ "github.com/wagoodman/go-progress"
- grypeDB "github.com/anchore/grype/grype/db/v5"
+ "github.com/anchore/archiver/v3"
+ "github.com/anchore/clio"
+ v5 "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/db/v5/store"
"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"
)
const (
- FileName = grypeDB.VulnerabilityStoreFileName
+ FileName = v5.VulnerabilityStoreFileName
+ lastUpdateCheckFileName = "last_update_check"
)
type Config struct {
- DBRootDir string
- ListingURL string
- CACert string
- ValidateByHashOnGet bool
- ValidateAge bool
- MaxAllowedBuiltAge time.Duration
+ ID clio.Identification
+ DBRootDir string
+ ListingURL string
+ CACert string
+ ValidateByHashOnGet bool
+ ValidateAge bool
+ MaxAllowedBuiltAge time.Duration
+ RequireUpdateCheck bool
+ ListingFileTimeout time.Duration
+ UpdateTimeout time.Duration
+ UpdateCheckMaxFrequency time.Duration
}
type Curator struct {
- fs afero.Fs
- downloader file.Getter
- targetSchema int
- dbDir string
- dbPath string
- listingURL string
- validateByHashOnGet bool
- validateAge bool
- maxAllowedBuiltAge time.Duration
+ fs afero.Fs
+ listingDownloader file.Getter
+ updateDownloader file.Getter
+ targetSchema int
+ dbDir string
+ dbPath string
+ listingURL string
+ validateByHashOnGet bool
+ validateAge bool
+ maxAllowedBuiltAge time.Duration
+ requireUpdateCheck bool
+ updateCheckMaxFrequency time.Duration
}
func NewCurator(cfg Config) (Curator, error) {
- dbDir := path.Join(cfg.DBRootDir, strconv.Itoa(vulnerability.SchemaVersion))
+ dbDir := path.Join(cfg.DBRootDir, strconv.Itoa(v5.SchemaVersion))
fs := afero.NewOsFs()
- httpClient, err := defaultHTTPClient(fs, cfg.CACert)
+ listingClient, err := defaultHTTPClient(fs, cfg.CACert)
if err != nil {
return Curator{}, err
}
+ listingClient.Timeout = cfg.ListingFileTimeout
+
+ dbClient, err := defaultHTTPClient(fs, cfg.CACert)
+ if err != nil {
+ return Curator{}, err
+ }
+ dbClient.Timeout = cfg.UpdateTimeout
return Curator{
- fs: fs,
- targetSchema: vulnerability.SchemaVersion,
- downloader: file.NewGetter(httpClient),
- dbDir: dbDir,
- dbPath: path.Join(dbDir, FileName),
- listingURL: cfg.ListingURL,
- validateByHashOnGet: cfg.ValidateByHashOnGet,
- validateAge: cfg.ValidateAge,
- maxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
+ fs: fs,
+ targetSchema: v5.SchemaVersion,
+ listingDownloader: file.NewGetter(cfg.ID, listingClient),
+ updateDownloader: file.NewGetter(cfg.ID, dbClient),
+ dbDir: dbDir,
+ dbPath: path.Join(dbDir, FileName),
+ listingURL: cfg.ListingURL,
+ validateByHashOnGet: cfg.ValidateByHashOnGet,
+ validateAge: cfg.ValidateAge,
+ maxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
+ requireUpdateCheck: cfg.RequireUpdateCheck,
+ updateCheckMaxFrequency: cfg.UpdateCheckMaxFrequency,
}, nil
}
@@ -77,15 +96,14 @@ func (c Curator) SupportedSchema() int {
return c.targetSchema
}
-func (c *Curator) GetStore() (grypeDB.StoreReader, grypeDB.DBCloser, error) {
+func (c *Curator) GetStore() (v5.StoreReader, error) {
// ensure the DB is ok
_, err := c.validateIntegrity(c.dbDir)
if err != nil {
- return nil, nil, fmt.Errorf("vulnerability database is invalid (run db update to correct): %+v", err)
+ return nil, fmt.Errorf("vulnerability database is invalid (run db update to correct): %+v", err)
}
- s, err := store.New(c.dbPath, false)
- return s, s, err
+ return store.New(c.dbPath, false)
}
func (c *Curator) Status() Status {
@@ -106,7 +124,7 @@ func (c *Curator) Status() Status {
SchemaVersion: metadata.Version,
Location: c.dbDir,
Checksum: metadata.Checksum,
- Err: c.Validate(),
+ Err: nil,
}
}
@@ -116,12 +134,17 @@ func (c *Curator) Delete() error {
}
// Update the existing DB, returning an indication if any action was taken.
-func (c *Curator) Update() (bool, error) {
+func (c *Curator) Update() (bool, error) { // nolint: funlen
+ if !c.isUpdateCheckAllowed() {
+ // we should not notify the user of an update check if the current configuration and state
+ // indicates we're should be in a low-pass filter mode (and the check frequency is too high).
+ // this should appear to the user as if we never attempted to check for an update at all.
+ return false, nil
+ }
+
// let consumers know of a monitorable event (download + import stages)
importProgress := progress.NewManual(1)
- stage := &progress.Stage{
- Current: "checking for update",
- }
+ stage := progress.NewAtomicStage("checking for update")
downloadProgress := progress.NewManual(1)
aggregateProgress := progress.NewAggregator(progress.DefaultStrategy, downloadProgress, importProgress)
@@ -139,19 +162,25 @@ func (c *Curator) Update() (bool, error) {
defer downloadProgress.SetCompleted()
defer importProgress.SetCompleted()
- updateAvailable, metadata, updateEntry, err := c.IsUpdateAvailable()
- if err != nil {
- // we want to continue if possible even if we can't check for an update
+ updateAvailable, metadata, updateEntry, checkErr := c.IsUpdateAvailable()
+ if checkErr != nil {
+ if c.requireUpdateCheck {
+ return false, fmt.Errorf("check for vulnerability database update failed: %w", checkErr)
+ }
log.Warnf("unable to check for vulnerability database update")
- log.Debugf("check for vulnerability update failed: %+v", err)
+ log.Debugf("check for vulnerability update failed: %+v", checkErr)
}
+
if updateAvailable {
log.Infof("downloading new vulnerability DB")
- err = c.UpdateTo(updateEntry, downloadProgress, importProgress, stage)
+ err := c.UpdateTo(updateEntry, downloadProgress, importProgress, stage)
if err != nil {
return false, fmt.Errorf("unable to update vulnerability database: %w", err)
}
+ // only set the last successful update check if the update was successful
+ c.setLastSuccessfulUpdateCheck()
+
if metadata != nil {
log.Infof(
"updated vulnerability DB from version=%d built=%q to version=%d built=%q",
@@ -171,10 +200,88 @@ func (c *Curator) Update() (bool, error) {
return true, nil
}
- stage.Current = "no update available"
+ // there was no update (or any issue while checking for an update)
+ if checkErr == nil {
+ c.setLastSuccessfulUpdateCheck()
+ }
+
+ stage.Set("no update available")
return false, nil
}
+func (c Curator) isUpdateCheckAllowed() bool {
+ if c.updateCheckMaxFrequency == 0 {
+ log.Trace("no max-frequency set for update check")
+ return true
+ }
+
+ elapsed, err := c.durationSinceUpdateCheck()
+ if err != nil {
+ // we had an IO error (or similar) trying to read or parse the file, we should not block the update check.
+ log.WithFields("error", err).Trace("unable to determine if update check is allowed")
+ return true
+ }
+ if elapsed == nil {
+ // there was no last check (this is a first run case), we should not block the update check.
+ return true
+ }
+
+ return *elapsed > c.updateCheckMaxFrequency
+}
+
+func (c Curator) durationSinceUpdateCheck() (*time.Duration, error) {
+ // open `$dbDir/last_update_check` file and read the timestamp and do now() - timestamp
+
+ filePath := path.Join(c.dbDir, lastUpdateCheckFileName)
+
+ if _, err := c.fs.Stat(filePath); os.IsNotExist(err) {
+ log.Trace("first-run of DB update")
+ return nil, nil
+ }
+
+ fh, err := c.fs.OpenFile(filePath, os.O_RDONLY, 0)
+ if err != nil {
+ return nil, fmt.Errorf("unable to read last update check timestamp: %w", err)
+ }
+
+ defer fh.Close()
+
+ // read and parse rfc3339 timestamp
+ var lastCheckStr string
+ _, err = fmt.Fscanf(fh, "%s", &lastCheckStr)
+ if err != nil {
+ return nil, fmt.Errorf("unable to read last update check timestamp: %w", err)
+ }
+
+ lastCheck, err := time.Parse(time.RFC3339, lastCheckStr)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse last update check timestamp: %w", err)
+ }
+
+ if lastCheck.IsZero() {
+ return nil, fmt.Errorf("empty update check timestamp")
+ }
+
+ elapsed := time.Since(lastCheck)
+ return &elapsed, nil
+}
+
+func (c Curator) setLastSuccessfulUpdateCheck() {
+ // note: we should always assume the DB dir actually exists, otherwise let this operation fail (since having a DB
+ // is a prerequisite for a successful update).
+
+ filePath := path.Join(c.dbDir, lastUpdateCheckFileName)
+ fh, err := c.fs.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
+ if err != nil {
+ log.WithFields("error", err).Trace("unable to write last update check timestamp")
+ return
+ }
+
+ defer fh.Close()
+
+ _, _ = fmt.Fprintf(fh, "%s", time.Now().UTC().Format(time.RFC3339))
+}
+
// IsUpdateAvailable indicates if there is a new update available as a boolean, and returns the latest listing information
// available for this schema.
func (c *Curator) IsUpdateAvailable() (bool, *Metadata, *ListingEntry, error) {
@@ -207,26 +314,26 @@ func (c *Curator) IsUpdateAvailable() (bool, *Metadata, *ListingEntry, error) {
}
// UpdateTo updates the existing DB with the specific other version provided from a listing entry.
-func (c *Curator) UpdateTo(listing *ListingEntry, downloadProgress, importProgress *progress.Manual, stage *progress.Stage) error {
- stage.Current = "downloading"
+func (c *Curator) UpdateTo(listing *ListingEntry, downloadProgress, importProgress *progress.Manual, stage *progress.AtomicStage) error {
+ stage.Set("downloading")
// note: the temp directory is persisted upon download/validation/activation failure to allow for investigation
tempDir, err := c.download(listing, downloadProgress)
if err != nil {
return err
}
- stage.Current = "validating integrity"
+ stage.Set("validating integrity")
_, err = c.validateIntegrity(tempDir)
if err != nil {
return err
}
- stage.Current = "importing"
+ stage.Set("importing")
err = c.activate(tempDir)
if err != nil {
return err
}
- stage.Current = "updated"
+ stage.Set("updated")
importProgress.Set(importProgress.Size())
importProgress.SetCompleted()
@@ -285,7 +392,7 @@ func (c *Curator) download(listing *ListingEntry, downloadProgress *progress.Man
url.RawQuery = query.Encode()
// go-getter will automatically extract all files within the archive to the temp dir
- err = c.downloader.GetToDir(tempDir, listing.URL.String(), downloadProgress)
+ err = c.updateDownloader.GetToDir(tempDir, listing.URL.String(), downloadProgress)
if err != nil {
return "", fmt.Errorf("unable to download db: %w", err)
}
@@ -370,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)
@@ -377,7 +485,7 @@ func (c Curator) ListingFromURL() (Listing, error) {
}()
// download the listing file
- err = c.downloader.GetFile(tempFile.Name(), c.listingURL)
+ err = c.listingDownloader.GetFile(tempFile.Name(), c.listingURL)
if err != nil {
return Listing{}, fmt.Errorf("unable to download listing: %w", err)
}
@@ -392,6 +500,7 @@ func (c Curator) ListingFromURL() (Listing, error) {
func defaultHTTPClient(fs afero.Fs, caCertPath string) (*http.Client, error) {
httpClient := cleanhttp.DefaultClient()
+ httpClient.Timeout = 30 * time.Second
if caCertPath != "" {
rootCAs := x509.NewCertPool()
diff --git a/grype/db/v5/distribution/curator_test.go b/grype/db/v5/distribution/curator_test.go
new file mode 100644
index 00000000000..c1882d6d442
--- /dev/null
+++ b/grype/db/v5/distribution/curator_test.go
@@ -0,0 +1,768 @@
+package distribution
+
+import (
+ "archive/tar"
+ "bufio"
+ "bytes"
+ "compress/gzip"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/gookit/color"
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+ "github.com/wagoodman/go-progress"
+
+ "github.com/anchore/grype/internal/file"
+ "github.com/anchore/grype/internal/stringutil"
+)
+
+type testGetter struct {
+ file map[string]string
+ dir map[string]string
+ calls stringutil.StringSet
+ fs afero.Fs
+}
+
+func newTestGetter(fs afero.Fs, f, d map[string]string) *testGetter {
+ return &testGetter{
+ file: f,
+ dir: d,
+ calls: stringutil.NewStringSet(),
+ fs: fs,
+ }
+}
+
+// GetFile downloads the give URL into the given path. The URL must reference a single file.
+func (g *testGetter) GetFile(dst, src string, _ ...*progress.Manual) error {
+ g.calls.Add(src)
+ if _, ok := g.file[src]; !ok {
+ return fmt.Errorf("blerg, no file!")
+ }
+ return afero.WriteFile(g.fs, dst, []byte(g.file[src]), 0755)
+}
+
+// Get downloads the given URL into the given directory. The directory must already exist.
+func (g *testGetter) GetToDir(dst, src string, _ ...*progress.Manual) error {
+ g.calls.Add(src)
+ if _, ok := g.dir[src]; !ok {
+ return fmt.Errorf("blerg, no file!")
+ }
+ return afero.WriteFile(g.fs, dst, []byte(g.dir[src]), 0755)
+}
+
+func newTestCurator(tb testing.TB, fs afero.Fs, getter file.Getter, dbDir, metadataUrl string, validateDbHash bool) Curator {
+ c, err := NewCurator(Config{
+ DBRootDir: dbDir,
+ ListingURL: metadataUrl,
+ ValidateByHashOnGet: validateDbHash,
+ })
+
+ require.NoError(tb, err)
+
+ c.listingDownloader = getter
+ c.updateDownloader = getter
+ c.fs = fs
+
+ return c
+}
+
+func Test_defaultHTTPClientHasCert(t *testing.T) {
+ tests := []struct {
+ name string
+ hasCert bool
+ }{
+ {
+ name: "no custom cert should use default system root certs",
+ hasCert: false,
+ },
+ {
+ name: "should use single custom cert",
+ hasCert: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var certPath string
+ if test.hasCert {
+ certPath = generateCertFixture(t)
+ }
+
+ httpClient, err := defaultHTTPClient(afero.NewOsFs(), certPath)
+ require.NoError(t, err)
+
+ if test.hasCert {
+ require.NotNil(t, httpClient.Transport.(*http.Transport).TLSClientConfig)
+ assert.Len(t, httpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects(), 1)
+ } else {
+ assert.Nil(t, httpClient.Transport.(*http.Transport).TLSClientConfig)
+ }
+ })
+ }
+}
+
+func Test_defaultHTTPClientTimeout(t *testing.T) {
+ c, err := defaultHTTPClient(afero.NewMemMapFs(), "")
+ require.NoError(t, err)
+ assert.Equal(t, 30*time.Second, c.Timeout)
+}
+
+func generateCertFixture(t *testing.T) string {
+ path := "test-fixtures/tls/server.crt"
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ // fixture already exists...
+ return path
+ }
+
+ t.Log(color.Bold.Sprint("Generating Key/Cert Fixture"))
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Errorf("unable to get cwd: %+v", err)
+ }
+
+ cmd := exec.Command("make", "server.crt")
+ cmd.Dir = filepath.Join(cwd, "test-fixtures/tls")
+
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ t.Fatalf("could not get stderr: %+v", err)
+ }
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ t.Fatalf("could not get stdout: %+v", err)
+ }
+
+ err = cmd.Start()
+ if err != nil {
+ t.Fatalf("failed to start cmd: %+v", err)
+ }
+
+ show := func(label string, reader io.ReadCloser) {
+ scanner := bufio.NewScanner(reader)
+ scanner.Split(bufio.ScanLines)
+ for scanner.Scan() {
+ t.Logf("%s: %s", label, scanner.Text())
+ }
+ }
+ go show("out", stdout)
+ go show("err", stderr)
+
+ if err := cmd.Wait(); err != nil {
+ if exiterr, ok := err.(*exec.ExitError); ok {
+ // The program has exited with an exit code != 0
+
+ // This works on both Unix and Windows. Although package
+ // syscall is generally platform dependent, WaitStatus is
+ // defined for both Unix and Windows and in both cases has
+ // an ExitStatus() method with the same signature.
+ if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
+ if status.ExitStatus() != 0 {
+ t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
+ }
+ }
+ } else {
+ t.Fatalf("unable to get generate fixture result: %+v", err)
+ }
+ }
+ return path
+}
+
+func TestCuratorDownload(t *testing.T) {
+ tests := []struct {
+ name string
+ entry *ListingEntry
+ expectedURL string
+ err bool
+ }{
+ {
+ name: "download populates returned tempdir",
+ entry: &ListingEntry{
+ Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
+ URL: mustUrl(url.Parse("http://a-url/payload.tar.gz")),
+ Checksum: "sha256:deadbeefcafe",
+ },
+ expectedURL: "http://a-url/payload.tar.gz?checksum=sha256%3Adeadbeefcafe",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ metadataUrl := "http://metadata.io"
+ contents := "CONTENTS!!!"
+ files := map[string]string{}
+ dirs := map[string]string{
+ test.expectedURL: contents,
+ }
+ fs := afero.NewMemMapFs()
+ getter := newTestGetter(fs, files, dirs)
+ cur := newTestCurator(t, fs, getter, "/tmp/dbdir", metadataUrl, false)
+
+ path, err := cur.download(test.entry, &progress.Manual{})
+ if err != nil {
+ t.Fatalf("could not download entry: %+v", err)
+ }
+
+ if !getter.calls.Contains(test.expectedURL) {
+ t.Fatalf("never made the appropriate fetch call: %+v", getter.calls)
+ }
+
+ f, err := fs.Open(path)
+ if err != nil {
+ t.Fatalf("no db file: %+v", err)
+ }
+
+ actual, err := afero.ReadAll(f)
+ if err != nil {
+ t.Fatalf("bad db file read: %+v", err)
+ }
+
+ if string(actual) != contents {
+ t.Fatalf("bad contents: %+v", string(actual))
+ }
+ })
+ }
+}
+
+func TestCuratorValidate(t *testing.T) {
+ tests := []struct {
+ name string
+ fixture string
+ constraint int
+ cfgValidateDbHash bool
+ err bool
+ }{
+ {
+ name: "good checksum & good constraint",
+ fixture: "test-fixtures/curator-validate/good-checksum",
+ cfgValidateDbHash: true,
+ constraint: 1,
+ err: false,
+ },
+ {
+ name: "good checksum & bad constraint",
+ fixture: "test-fixtures/curator-validate/good-checksum",
+ cfgValidateDbHash: true,
+ constraint: 2,
+ err: true,
+ },
+ {
+ name: "bad checksum & good constraint",
+ fixture: "test-fixtures/curator-validate/bad-checksum",
+ cfgValidateDbHash: true,
+ constraint: 1,
+ err: true,
+ },
+ {
+ name: "bad checksum & bad constraint",
+ fixture: "test-fixtures/curator-validate/bad-checksum",
+ cfgValidateDbHash: true,
+ constraint: 2,
+ err: true,
+ },
+ {
+ name: "bad checksum ignored on config exception",
+ fixture: "test-fixtures/curator-validate/bad-checksum",
+ cfgValidateDbHash: false,
+ constraint: 1,
+ err: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ metadataUrl := "http://metadata.io"
+
+ fs := afero.NewOsFs()
+ getter := newTestGetter(fs, nil, nil)
+ cur := newTestCurator(t, fs, getter, "/tmp/dbdir", metadataUrl, test.cfgValidateDbHash)
+
+ cur.targetSchema = test.constraint
+
+ md, err := cur.validateIntegrity(test.fixture)
+
+ if err == nil && test.err {
+ t.Errorf("expected an error but got none")
+ } else if err != nil && !test.err {
+ assert.NotZero(t, md)
+ t.Errorf("expected no error, got: %+v", err)
+ }
+ })
+ }
+}
+
+func TestCuratorDBPathHasSchemaVersion(t *testing.T) {
+ fs := afero.NewMemMapFs()
+ dbRootPath := "/tmp/dbdir"
+ cur := newTestCurator(t, fs, nil, dbRootPath, "http://metadata.io", false)
+
+ assert.Equal(t, path.Join(dbRootPath, strconv.Itoa(cur.targetSchema)), cur.dbDir, "unexpected dir")
+ assert.Contains(t, cur.dbPath, path.Join(dbRootPath, strconv.Itoa(cur.targetSchema)), "unexpected path")
+}
+
+func TestCurator_validateStaleness(t *testing.T) {
+ type fields struct {
+ validateAge bool
+ maxAllowedDBAge time.Duration
+ md Metadata
+ }
+
+ now := time.Now().UTC()
+ tests := []struct {
+ name string
+ cur *Curator
+ fields fields
+ wantErr assert.ErrorAssertionFunc
+ }{
+ {
+ name: "no-validation",
+ fields: fields{
+ md: Metadata{Built: now},
+ },
+ wantErr: assert.NoError,
+ },
+ {
+ name: "up-to-date",
+ fields: fields{
+ maxAllowedDBAge: 2 * time.Hour,
+ validateAge: true,
+ md: Metadata{Built: now},
+ },
+ wantErr: assert.NoError,
+ },
+ {
+ name: "stale-data",
+ fields: fields{
+ maxAllowedDBAge: time.Hour,
+ validateAge: true,
+ md: Metadata{Built: now.UTC().Add(-4 * time.Hour)},
+ },
+ wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
+ return assert.ErrorContains(t, err, "the vulnerability database was built")
+ },
+ },
+ {
+ name: "stale-data-no-validation",
+ fields: fields{
+ maxAllowedDBAge: time.Hour,
+ validateAge: false,
+ md: Metadata{Built: now.Add(-4 * time.Hour)},
+ },
+ wantErr: assert.NoError,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Curator{
+ validateAge: tt.fields.validateAge,
+ maxAllowedBuiltAge: tt.fields.maxAllowedDBAge,
+ }
+ tt.wantErr(t, c.validateStaleness(tt.fields.md), fmt.Sprintf("validateStaleness(%v)", tt.fields.md))
+ })
+ }
+}
+
+func Test_requireUpdateCheck(t *testing.T) {
+ toJson := func(listing any) []byte {
+ listingContents := bytes.Buffer{}
+ enc := json.NewEncoder(&listingContents)
+ _ = enc.Encode(listing)
+ return listingContents.Bytes()
+ }
+ checksum := func(b []byte) string {
+ h := sha256.New()
+ h.Write(b)
+ return hex.EncodeToString(h.Sum(nil))
+ }
+ makeTarGz := func(mod time.Time, contents []byte) []byte {
+ metadata := toJson(MetadataJSON{
+ Built: mod.Format(time.RFC3339),
+ Version: 5,
+ Checksum: "sha256:" + checksum(contents),
+ })
+ tgz := bytes.Buffer{}
+ gz := gzip.NewWriter(&tgz)
+ w := tar.NewWriter(gz)
+ _ = w.WriteHeader(&tar.Header{
+ Name: "metadata.json",
+ Size: int64(len(metadata)),
+ Mode: 0600,
+ })
+ _, _ = w.Write(metadata)
+ _ = w.WriteHeader(&tar.Header{
+ Name: "vulnerability.db",
+ Size: int64(len(contents)),
+ Mode: 0600,
+ })
+ _, _ = w.Write(contents)
+ _ = w.Close()
+ _ = gz.Close()
+ return tgz.Bytes()
+ }
+
+ newTime := time.Date(2024, 06, 13, 17, 13, 13, 0, time.UTC)
+ midTime := time.Date(2022, 06, 13, 17, 13, 13, 0, time.UTC)
+ oldTime := time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC)
+
+ newDB := makeTarGz(newTime, []byte("some-good-contents"))
+
+ midMetadata := toJson(MetadataJSON{
+ Built: midTime.Format(time.RFC3339),
+ Version: 5,
+ Checksum: "sha256:deadbeefcafe",
+ })
+
+ var handlerFunc http.HandlerFunc
+
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ handlerFunc(w, r)
+ }))
+ defer srv.Close()
+
+ newDbURI := "/db.tar.gz"
+
+ newListing := toJson(Listing{Available: map[int][]ListingEntry{5: {ListingEntry{
+ Built: newTime,
+ URL: mustUrl(url.Parse(srv.URL + newDbURI)),
+ Checksum: "sha256:" + checksum(newDB),
+ }}}})
+
+ oldListing := toJson(Listing{Available: map[int][]ListingEntry{5: {ListingEntry{
+ Built: oldTime,
+ URL: mustUrl(url.Parse(srv.URL + newDbURI)),
+ Checksum: "sha256:" + checksum(newDB),
+ }}}})
+
+ newListingURI := "/listing.json"
+ oldListingURI := "/oldlisting.json"
+ badListingURI := "/badlisting.json"
+
+ handlerFunc = func(response http.ResponseWriter, request *http.Request) {
+ switch request.RequestURI {
+ case newListingURI:
+ response.WriteHeader(http.StatusOK)
+ _, _ = response.Write(newListing)
+ case oldListingURI:
+ response.WriteHeader(http.StatusOK)
+ _, _ = response.Write(oldListing)
+ case newDbURI:
+ response.WriteHeader(http.StatusOK)
+ _, _ = response.Write(newDB)
+ default:
+ http.Error(response, "not found", http.StatusNotFound)
+ }
+ }
+
+ tests := []struct {
+ name string
+ config Config
+ dbDir map[string][]byte
+ wantResult bool
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "listing with update",
+ config: Config{
+ ListingURL: srv.URL + newListingURI,
+ RequireUpdateCheck: true,
+ },
+ dbDir: map[string][]byte{
+ "5/metadata.json": midMetadata,
+ },
+ wantResult: true,
+ wantErr: require.NoError,
+ },
+ {
+ name: "no update",
+ config: Config{
+ ListingURL: srv.URL + oldListingURI,
+ RequireUpdateCheck: false,
+ },
+ dbDir: map[string][]byte{
+ "5/metadata.json": midMetadata,
+ },
+ wantResult: false,
+ wantErr: require.NoError,
+ },
+ {
+ name: "update error fail",
+ config: Config{
+ ListingURL: srv.URL + badListingURI,
+ RequireUpdateCheck: true,
+ },
+ wantResult: false,
+ wantErr: require.Error,
+ },
+ {
+ name: "update error continue",
+ config: Config{
+ ListingURL: srv.URL + badListingURI,
+ RequireUpdateCheck: false,
+ },
+ wantResult: false,
+ wantErr: require.NoError,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ dbTmpDir := t.TempDir()
+ tt.config.DBRootDir = dbTmpDir
+ tt.config.ListingFileTimeout = 1 * time.Minute
+ tt.config.UpdateTimeout = 1 * time.Minute
+ for filePath, contents := range tt.dbDir {
+ fullPath := filepath.Join(dbTmpDir, filepath.FromSlash(filePath))
+ err := os.MkdirAll(filepath.Dir(fullPath), 0700|os.ModeDir)
+ require.NoError(t, err)
+ err = os.WriteFile(fullPath, contents, 0700)
+ require.NoError(t, err)
+ }
+ c, err := NewCurator(tt.config)
+ require.NoError(t, err)
+
+ result, err := c.Update()
+ require.Equal(t, tt.wantResult, result)
+ tt.wantErr(t, err)
+ })
+ }
+}
+
+func TestCuratorTimeoutBehavior(t *testing.T) {
+ failAfter := 10 * time.Second
+ success := make(chan struct{})
+ errs := make(chan error)
+ timeout := time.After(failAfter)
+
+ hangForeverHandler := func(w http.ResponseWriter, r *http.Request) {
+ select {} // hang forever
+ }
+
+ ts := httptest.NewServer(http.HandlerFunc(hangForeverHandler))
+
+ cfg := Config{
+ DBRootDir: "",
+ ListingURL: fmt.Sprintf("%s/listing.json", ts.URL),
+ CACert: "",
+ ValidateByHashOnGet: false,
+ ValidateAge: false,
+ MaxAllowedBuiltAge: 0,
+ ListingFileTimeout: 400 * time.Millisecond,
+ UpdateTimeout: 400 * time.Millisecond,
+ }
+
+ curator, err := NewCurator(cfg)
+ require.NoError(t, err)
+
+ u, err := url.Parse(fmt.Sprintf("%s/some-db.tar.gz", ts.URL))
+ require.NoError(t, err)
+
+ entry := ListingEntry{
+ Built: time.Now(),
+ Version: 5,
+ URL: u,
+ Checksum: "83b52a2aa6aff35d208520f40dd36144",
+ }
+
+ downloadProgress := progress.NewManual(10)
+ importProgress := progress.NewManual(10)
+ stage := progress.NewAtomicStage("some-stage")
+
+ runTheTest := func(success chan struct{}, errs chan error) {
+ _, _, _, err = curator.IsUpdateAvailable()
+ if err == nil {
+ errs <- errors.New("expected timeout error but got nil")
+ return
+ }
+ if !strings.Contains(err.Error(), "Timeout exceeded") {
+ errs <- fmt.Errorf("expected %q but got %q", "Timeout exceeded", err.Error())
+ return
+ }
+
+ err = curator.UpdateTo(&entry, downloadProgress, importProgress, stage)
+ if err == nil {
+ errs <- errors.New("expected timeout error but got nil")
+ return
+ }
+ if !strings.Contains(err.Error(), "Timeout exceeded") {
+ errs <- fmt.Errorf("expected %q but got %q", "Timeout exceeded", err.Error())
+ return
+ }
+ success <- struct{}{}
+ }
+ go runTheTest(success, errs)
+
+ select {
+ case <-success:
+ return
+ case err := <-errs:
+ t.Error(err)
+ case <-timeout:
+ t.Fatalf("timeout exceeded (%v)", failAfter)
+ }
+}
+
+func TestCurator_IsUpdateCheckAllowed(t *testing.T) {
+ fs := afero.NewOsFs()
+ tempDir := t.TempDir()
+
+ curator := Curator{
+ fs: fs,
+ updateCheckMaxFrequency: 10 * time.Minute,
+ dbDir: tempDir,
+ }
+
+ writeLastCheckTime := func(t *testing.T, lastCheckTime time.Time) {
+ err := afero.WriteFile(fs, path.Join(tempDir, lastUpdateCheckFileName), []byte(lastCheckTime.Format(time.RFC3339)), 0644)
+ require.NoError(t, err)
+ }
+
+ t.Run("first run check (no last check file)", func(t *testing.T) {
+ require.True(t, curator.isUpdateCheckAllowed())
+ })
+
+ t.Run("check not allowed due to frequency", func(t *testing.T) {
+ writeLastCheckTime(t, time.Now().Add(-5*time.Minute))
+
+ require.False(t, curator.isUpdateCheckAllowed())
+ })
+
+ t.Run("check allowed after the frequency period", func(t *testing.T) {
+ writeLastCheckTime(t, time.Now().Add(-20*time.Minute))
+
+ require.True(t, curator.isUpdateCheckAllowed())
+ })
+}
+
+func TestCurator_DurationSinceUpdateCheck(t *testing.T) {
+ fs := afero.NewOsFs()
+ tempDir := t.TempDir()
+
+ curator := Curator{
+ fs: fs,
+ dbDir: tempDir,
+ }
+
+ writeLastCheckTime := func(t *testing.T, lastCheckTime time.Time) {
+ err := afero.WriteFile(fs, path.Join(tempDir, lastUpdateCheckFileName), []byte(lastCheckTime.Format(time.RFC3339)), 0644)
+ require.NoError(t, err)
+ }
+
+ t.Run("no last check file", func(t *testing.T) {
+ elapsed, err := curator.durationSinceUpdateCheck()
+ require.NoError(t, err)
+ require.Nil(t, elapsed)
+ })
+
+ t.Run("last check file does not exist", func(t *testing.T) {
+ // simulate a non-existing file
+ _, err := curator.durationSinceUpdateCheck()
+ require.NoError(t, err)
+ })
+
+ t.Run("valid last check file", func(t *testing.T) {
+ writeLastCheckTime(t, time.Now().Add(-5*time.Minute))
+
+ elapsed, err := curator.durationSinceUpdateCheck()
+ require.NoError(t, err)
+ require.NotNil(t, elapsed)
+ require.True(t, *elapsed >= 5*time.Minute)
+ })
+
+ t.Run("malformed last check file", func(t *testing.T) {
+ err := afero.WriteFile(fs, path.Join(tempDir, lastUpdateCheckFileName), []byte("not a timestamp"), 0644)
+ require.NoError(t, err)
+
+ _, err = curator.durationSinceUpdateCheck()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unable to parse last update check timestamp")
+ })
+}
+
+func TestCurator_SetLastSuccessfulUpdateCheck(t *testing.T) {
+ fs := afero.NewOsFs()
+ tempDir := t.TempDir()
+
+ curator := Curator{
+ fs: fs,
+ dbDir: tempDir,
+ }
+
+ t.Run("set last successful update check", func(t *testing.T) {
+ curator.setLastSuccessfulUpdateCheck()
+
+ data, err := afero.ReadFile(fs, path.Join(tempDir, lastUpdateCheckFileName))
+ require.NoError(t, err)
+
+ lastCheckTime, err := time.Parse(time.RFC3339, string(data))
+ require.NoError(t, err)
+ require.WithinDuration(t, time.Now().UTC(), lastCheckTime, time.Second)
+ })
+
+ t.Run("error writing last successful update check", func(t *testing.T) {
+ invalidFs := afero.NewReadOnlyFs(fs) // make it read-only, which should simular a write error
+ curator.fs = invalidFs
+
+ curator.setLastSuccessfulUpdateCheck()
+ })
+}
+
+// Mock for the file.Getter interface
+type MockGetter struct {
+ mock.Mock
+}
+
+func (m *MockGetter) GetFile(dst, src string, monitor ...*progress.Manual) error {
+ args := m.Called(dst, src, monitor)
+ return args.Error(0)
+}
+
+func (m *MockGetter) GetToDir(dst, src string, monitor ...*progress.Manual) error {
+ args := m.Called(dst, src, monitor)
+ return args.Error(0)
+}
+
+func TestCurator_Update_setLastSuccessfulUpdateCheck_notCalled(t *testing.T) {
+
+ newCurator := func(t *testing.T) *Curator {
+ return &Curator{
+ fs: afero.NewOsFs(),
+ dbDir: t.TempDir(),
+ updateCheckMaxFrequency: 10 * time.Minute,
+ listingDownloader: &MockGetter{},
+ updateDownloader: &MockGetter{},
+ requireUpdateCheck: true,
+ }
+ }
+
+ t.Run("error checking for update", func(t *testing.T) {
+ c := newCurator(t)
+
+ c.listingDownloader.(*MockGetter).On("GetFile", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("get listing failed"))
+
+ _, err := c.Update()
+ require.Error(t, err)
+ require.ErrorContains(t, err, "get listing failed")
+
+ require.NoFileExists(t, filepath.Join(t.TempDir(), lastUpdateCheckFileName))
+ })
+
+}
diff --git a/grype/db/listing.go b/grype/db/v5/distribution/listing.go
similarity index 99%
rename from grype/db/listing.go
rename to grype/db/v5/distribution/listing.go
index 3930a736846..d5ace7c1799 100644
--- a/grype/db/listing.go
+++ b/grype/db/v5/distribution/listing.go
@@ -1,4 +1,4 @@
-package db
+package distribution
import (
"encoding/json"
diff --git a/grype/db/listing_entry.go b/grype/db/v5/distribution/listing_entry.go
similarity index 99%
rename from grype/db/listing_entry.go
rename to grype/db/v5/distribution/listing_entry.go
index 9b928e6fe3c..6f334e7f4ad 100644
--- a/grype/db/listing_entry.go
+++ b/grype/db/v5/distribution/listing_entry.go
@@ -1,4 +1,4 @@
-package db
+package distribution
import (
"crypto/sha256"
diff --git a/grype/db/listing_test.go b/grype/db/v5/distribution/listing_test.go
similarity index 99%
rename from grype/db/listing_test.go
rename to grype/db/v5/distribution/listing_test.go
index 673b78a8553..44f3d4a441f 100644
--- a/grype/db/listing_test.go
+++ b/grype/db/v5/distribution/listing_test.go
@@ -1,4 +1,4 @@
-package db
+package distribution
import (
"net/url"
diff --git a/grype/db/metadata.go b/grype/db/v5/distribution/metadata.go
similarity index 99%
rename from grype/db/metadata.go
rename to grype/db/v5/distribution/metadata.go
index 60fc6702583..70e451fbb01 100644
--- a/grype/db/metadata.go
+++ b/grype/db/v5/distribution/metadata.go
@@ -1,4 +1,4 @@
-package db
+package distribution
import (
"encoding/json"
diff --git a/grype/db/metadata_test.go b/grype/db/v5/distribution/metadata_test.go
similarity index 99%
rename from grype/db/metadata_test.go
rename to grype/db/v5/distribution/metadata_test.go
index bd94cb65681..e6a2feb5540 100644
--- a/grype/db/metadata_test.go
+++ b/grype/db/v5/distribution/metadata_test.go
@@ -1,4 +1,4 @@
-package db
+package distribution
import (
"testing"
diff --git a/grype/db/status.go b/grype/db/v5/distribution/status.go
similarity index 68%
rename from grype/db/status.go
rename to grype/db/v5/distribution/status.go
index 7f90ab98ccb..65e27ea9dab 100644
--- a/grype/db/status.go
+++ b/grype/db/v5/distribution/status.go
@@ -1,4 +1,4 @@
-package db
+package distribution
import "time"
@@ -9,3 +9,10 @@ type Status struct {
Checksum string `json:"checksum"`
Err error `json:"error"`
}
+
+func (s Status) Status() string {
+ if s.Err != nil {
+ return "invalid"
+ }
+ return "valid"
+}
diff --git a/grype/db/test-fixtures/curator-validate/bad-checksum/metadata.json b/grype/db/v5/distribution/test-fixtures/curator-validate/bad-checksum/metadata.json
similarity index 100%
rename from grype/db/test-fixtures/curator-validate/bad-checksum/metadata.json
rename to grype/db/v5/distribution/test-fixtures/curator-validate/bad-checksum/metadata.json
diff --git a/grype/db/test-fixtures/curator-validate/bad-checksum/vulnerability.db b/grype/db/v5/distribution/test-fixtures/curator-validate/bad-checksum/vulnerability.db
similarity index 100%
rename from grype/db/test-fixtures/curator-validate/bad-checksum/vulnerability.db
rename to grype/db/v5/distribution/test-fixtures/curator-validate/bad-checksum/vulnerability.db
diff --git a/grype/db/test-fixtures/curator-validate/good-checksum/metadata.json b/grype/db/v5/distribution/test-fixtures/curator-validate/good-checksum/metadata.json
similarity index 100%
rename from grype/db/test-fixtures/curator-validate/good-checksum/metadata.json
rename to grype/db/v5/distribution/test-fixtures/curator-validate/good-checksum/metadata.json
diff --git a/grype/db/test-fixtures/curator-validate/good-checksum/vulnerability.db b/grype/db/v5/distribution/test-fixtures/curator-validate/good-checksum/vulnerability.db
similarity index 100%
rename from grype/db/test-fixtures/curator-validate/good-checksum/vulnerability.db
rename to grype/db/v5/distribution/test-fixtures/curator-validate/good-checksum/vulnerability.db
diff --git a/grype/db/test-fixtures/listing-sorted.json b/grype/db/v5/distribution/test-fixtures/listing-sorted.json
similarity index 100%
rename from grype/db/test-fixtures/listing-sorted.json
rename to grype/db/v5/distribution/test-fixtures/listing-sorted.json
diff --git a/grype/db/test-fixtures/listing-unsorted.json b/grype/db/v5/distribution/test-fixtures/listing-unsorted.json
similarity index 100%
rename from grype/db/test-fixtures/listing-unsorted.json
rename to grype/db/v5/distribution/test-fixtures/listing-unsorted.json
diff --git a/grype/db/test-fixtures/listing.json b/grype/db/v5/distribution/test-fixtures/listing.json
similarity index 100%
rename from grype/db/test-fixtures/listing.json
rename to grype/db/v5/distribution/test-fixtures/listing.json
diff --git a/grype/db/test-fixtures/metadata-edt-timezone/metadata.json b/grype/db/v5/distribution/test-fixtures/metadata-edt-timezone/metadata.json
similarity index 100%
rename from grype/db/test-fixtures/metadata-edt-timezone/metadata.json
rename to grype/db/v5/distribution/test-fixtures/metadata-edt-timezone/metadata.json
diff --git a/grype/db/test-fixtures/metadata-gocase/metadata.json b/grype/db/v5/distribution/test-fixtures/metadata-gocase/metadata.json
similarity index 100%
rename from grype/db/test-fixtures/metadata-gocase/metadata.json
rename to grype/db/v5/distribution/test-fixtures/metadata-gocase/metadata.json
diff --git a/grype/db/test-fixtures/tls/.gitignore b/grype/db/v5/distribution/test-fixtures/tls/.gitignore
similarity index 100%
rename from grype/db/test-fixtures/tls/.gitignore
rename to grype/db/v5/distribution/test-fixtures/tls/.gitignore
diff --git a/grype/db/test-fixtures/tls/Makefile b/grype/db/v5/distribution/test-fixtures/tls/Makefile
similarity index 100%
rename from grype/db/test-fixtures/tls/Makefile
rename to grype/db/v5/distribution/test-fixtures/tls/Makefile
diff --git a/grype/db/test-fixtures/tls/README.md b/grype/db/v5/distribution/test-fixtures/tls/README.md
similarity index 100%
rename from grype/db/test-fixtures/tls/README.md
rename to grype/db/v5/distribution/test-fixtures/tls/README.md
diff --git a/grype/db/test-fixtures/tls/generate-x509-cert-pair.sh b/grype/db/v5/distribution/test-fixtures/tls/generate-x509-cert-pair.sh
similarity index 100%
rename from grype/db/test-fixtures/tls/generate-x509-cert-pair.sh
rename to grype/db/v5/distribution/test-fixtures/tls/generate-x509-cert-pair.sh
diff --git a/grype/db/test-fixtures/tls/listing.py b/grype/db/v5/distribution/test-fixtures/tls/listing.py
similarity index 100%
rename from grype/db/test-fixtures/tls/listing.py
rename to grype/db/v5/distribution/test-fixtures/tls/listing.py
diff --git a/grype/db/test-fixtures/tls/serve.py b/grype/db/v5/distribution/test-fixtures/tls/serve.py
similarity index 100%
rename from grype/db/test-fixtures/tls/serve.py
rename to grype/db/v5/distribution/test-fixtures/tls/serve.py
diff --git a/grype/db/match_exclusion_provider.go b/grype/db/v5/match_exclusion_provider.go
similarity index 74%
rename from grype/db/match_exclusion_provider.go
rename to grype/db/v5/match_exclusion_provider.go
index 113dc5502dc..a82dc4d3c9f 100644
--- a/grype/db/match_exclusion_provider.go
+++ b/grype/db/v5/match_exclusion_provider.go
@@ -1,25 +1,24 @@
-package db
+package v5
import (
"fmt"
- grypeDB "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/match"
)
var _ match.ExclusionProvider = (*MatchExclusionProvider)(nil)
type MatchExclusionProvider struct {
- reader grypeDB.VulnerabilityMatchExclusionStoreReader
+ reader VulnerabilityMatchExclusionStoreReader
}
-func NewMatchExclusionProvider(reader grypeDB.VulnerabilityMatchExclusionStoreReader) *MatchExclusionProvider {
+func NewMatchExclusionProvider(reader VulnerabilityMatchExclusionStoreReader) *MatchExclusionProvider {
return &MatchExclusionProvider{
reader: reader,
}
}
-func buildIgnoreRulesFromMatchExclusion(e grypeDB.VulnerabilityMatchExclusion) []match.IgnoreRule {
+func buildIgnoreRulesFromMatchExclusion(e VulnerabilityMatchExclusion) []match.IgnoreRule {
var ignoreRules []match.IgnoreRule
if len(e.Constraints) == 0 {
@@ -45,7 +44,7 @@ func buildIgnoreRulesFromMatchExclusion(e grypeDB.VulnerabilityMatchExclusion) [
return ignoreRules
}
-func (pr *MatchExclusionProvider) GetRules(vulnerabilityID string) ([]match.IgnoreRule, error) {
+func (pr *MatchExclusionProvider) IgnoreRules(vulnerabilityID string) ([]match.IgnoreRule, error) {
matchExclusions, err := pr.reader.GetVulnerabilityMatchExclusion(vulnerabilityID)
if err != nil {
return nil, fmt.Errorf("match exclusion provider failed to fetch records for vulnerability id='%s': %w", vulnerabilityID, err)
diff --git a/grype/db/v5/namespace/cpe/namespace.go b/grype/db/v5/namespace/cpe/namespace.go
index baa4ff892be..1045be0ae1a 100644
--- a/grype/db/v5/namespace/cpe/namespace.go
+++ b/grype/db/v5/namespace/cpe/namespace.go
@@ -29,13 +29,16 @@ func FromString(namespaceStr string) (*Namespace, error) {
}
components := strings.Split(namespaceStr, ":")
+ return FromComponents(components)
+}
+func FromComponents(components []string) (*Namespace, error) {
if len(components) != 2 {
- return nil, fmt.Errorf("unable to create CPE namespace from %s: incorrect number of components", namespaceStr)
+ return nil, fmt.Errorf("unable to create CPE namespace from %s: incorrect number of components", strings.Join(components, ":"))
}
if components[1] != ID {
- return nil, fmt.Errorf("unable to create CPE namespace from %s: type %s is incorrect", namespaceStr, components[1])
+ return nil, fmt.Errorf("unable to create CPE namespace from %s: type %s is incorrect", strings.Join(components, ":"), components[1])
}
return NewNamespace(components[0]), nil
diff --git a/grype/db/v5/namespace/distro/namespace.go b/grype/db/v5/namespace/distro/namespace.go
index 9bfefa3e939..083174afed7 100644
--- a/grype/db/v5/namespace/distro/namespace.go
+++ b/grype/db/v5/namespace/distro/namespace.go
@@ -34,13 +34,16 @@ func FromString(namespaceStr string) (*Namespace, error) {
}
components := strings.Split(namespaceStr, ":")
+ return FromComponents(components)
+}
+func FromComponents(components []string) (*Namespace, error) {
if len(components) != 4 {
- return nil, fmt.Errorf("unable to create distro namespace from %s: incorrect number of components", namespaceStr)
+ return nil, fmt.Errorf("unable to create distro namespace from %s: incorrect number of components", strings.Join(components, ":"))
}
if components[1] != ID {
- return nil, fmt.Errorf("unable to create distro namespace from %s: type %s is incorrect", namespaceStr, components[1])
+ return nil, fmt.Errorf("unable to create distro namespace from %s: type %s is incorrect", strings.Join(components, ":"), components[1])
}
return NewNamespace(components[0], distro.Type(components[2]), components[3]), nil
diff --git a/grype/db/v5/namespace/from_string.go b/grype/db/v5/namespace/from_string.go
index bf68a38643d..b71dd11e31e 100644
--- a/grype/db/v5/namespace/from_string.go
+++ b/grype/db/v5/namespace/from_string.go
@@ -17,17 +17,17 @@ func FromString(namespaceStr string) (Namespace, error) {
components := strings.Split(namespaceStr, ":")
- if len(components) < 1 {
+ if len(components) < 2 {
return nil, fmt.Errorf("unable to create namespace from %s: incorrect number of components", namespaceStr)
}
switch components[1] {
case cpe.ID:
- return cpe.FromString(namespaceStr)
+ return cpe.FromComponents(components)
case distro.ID:
- return distro.FromString(namespaceStr)
+ return distro.FromComponents(components)
case language.ID:
- return language.FromString(namespaceStr)
+ return language.FromComponents(components)
default:
return nil, fmt.Errorf("unable to create namespace from %s: unknown type %s", namespaceStr, components[1])
}
diff --git a/grype/db/v5/namespace/index.go b/grype/db/v5/namespace/index.go
deleted file mode 100644
index 4f4f6db1873..00000000000
--- a/grype/db/v5/namespace/index.go
+++ /dev/null
@@ -1,176 +0,0 @@
-package namespace
-
-import (
- "fmt"
- "regexp"
- "strings"
-
- "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/log"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-var alpineVersionRegularExpression = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)$`)
-
-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
- }
-
- if d.IsRolling() {
- distroKey := fmt.Sprintf("%s:%s", strings.ToLower(d.Type.String()), "rolling")
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
- }
-
- var versionSegments []int
- if d.Version != nil {
- versionSegments = d.Version.Segments()
- }
-
- if len(versionSegments) > 0 {
- // Alpine is a special case since we can only match on x.y.z
- // after this things like x.y and x are valid namespace selections
- if d.Type == grypeDistro.Alpine {
- if v := getAlpineNamespace(i, d, versionSegments); v != nil {
- return v
- }
- }
-
- // Next attempt a direct match on distro full name and version
- distroKey := fmt.Sprintf("%s:%s", strings.ToLower(d.Type.String()), d.FullVersion())
-
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
-
- if len(versionSegments) == 3 {
- // Try with only first two version components
- distroKey = fmt.Sprintf("%s:%d.%d", strings.ToLower(d.Type.String()), versionSegments[0], versionSegments[1])
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
-
- // Try using only major version component
- distroKey = fmt.Sprintf("%s:%d", strings.ToLower(d.Type.String()), versionSegments[0])
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
- }
-
- // Fall back into the manual mapping logic derived from
- // https://github.com/anchore/enterprise/blob/eb71bc6686b9f4c92347a4e95bec828cee879197/anchore_engine/services/policy_engine/__init__.py#L127-L140
- switch d.Type {
- case grypeDistro.CentOS, grypeDistro.RedHat, grypeDistro.Fedora, grypeDistro.RockyLinux, grypeDistro.AlmaLinux, grypeDistro.Gentoo:
- // TODO: there is no mapping of fedora version to RHEL latest version (only the name)
- distroKey = fmt.Sprintf("%s:%d", strings.ToLower(string(grypeDistro.RedHat)), versionSegments[0])
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
- }
- }
-
- // Fall back to alpine:edge if no version segments found
- // alpine:edge is labeled as alpine-x.x_alphaYYYYMMDD
- if versionSegments == nil && d.Type == grypeDistro.Alpine {
- distroKey := fmt.Sprintf("%s:%s", strings.ToLower(d.Type.String()), "edge")
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
- }
-
- return nil
-}
-
-func getAlpineNamespace(i *Index, d *grypeDistro.Distro, versionSegments []int) []*distro.Namespace {
- // check if distro version matches x.y.z
- if alpineVersionRegularExpression.Match([]byte(d.RawVersion)) {
- // Get the first two version components
- // TODO: should we update the namespaces in db generation to match x.y.z here?
- distroKey := fmt.Sprintf("%s:%d.%d", strings.ToLower(d.Type.String()), versionSegments[0], versionSegments[1])
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
- }
-
- // If the version does not match x.y.z then it is edge
- // In this case it would have - or _ alpha,beta,etc
- // https://github.com/anchore/grype/issues/964#issuecomment-1290888755
- distroKey := fmt.Sprintf("%s:%s", strings.ToLower(d.Type.String()), "edge")
- if v, ok := i.byDistroKey[distroKey]; ok {
- return v
- }
-
- return nil
-}
-
-func (i *Index) CPENamespaces() []*cpe.Namespace {
- return i.cpe
-}
diff --git a/grype/db/v5/namespace/index_test.go b/grype/db/v5/namespace/index_test.go
deleted file mode 100644
index 64ce00c4a79..00000000000
--- a/grype/db/v5/namespace/index_test.go
+++ /dev/null
@@ -1,351 +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",
- "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.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 {
- distro, err := osDistro.New(dt, v, idLikes...)
- assert.NoError(t, err)
- return distro
-}
-
-func TestIndex_NamespacesForDistro(t *testing.T) {
- namespaceIndex, err := FromStrings([]string{
- "alpine:distro:alpine:3.15",
- "alpine:distro:alpine:3.16",
- "alpine:distro:alpine:edge",
- "debian:distro:debian:8",
- "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",
- "msrc:distro:windows:471816",
- "ubuntu:distro:ubuntu:18.04",
- "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 minor version with no patch should match edge",
- distro: newDistro(t, osDistro.Alpine, "3.16", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("alpine", osDistro.Alpine, "edge"),
- },
- },
- {
- 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 no namespace",
- distro: newDistro(t, osDistro.Alpine, "3.16.4.5", []string{}),
- namespaces: []*distro.Namespace{
- distro.NewNamespace("alpine", osDistro.Alpine, "edge"),
- },
- },
- {
- 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: "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,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- namespaces := namespaceIndex.NamespacesForDistro(test.distro)
- assert.Equal(t, test.namespaces, namespaces)
- })
- }
-}
diff --git a/grype/db/v5/namespace/language/namespace.go b/grype/db/v5/namespace/language/namespace.go
index 2a1814eb131..f438bbcb5dc 100644
--- a/grype/db/v5/namespace/language/namespace.go
+++ b/grype/db/v5/namespace/language/namespace.go
@@ -35,13 +35,16 @@ func FromString(namespaceStr string) (*Namespace, error) {
}
components := strings.Split(namespaceStr, ":")
+ return FromComponents(components)
+}
+func FromComponents(components []string) (*Namespace, error) {
if len(components) != 3 && len(components) != 4 {
- return nil, fmt.Errorf("unable to create language namespace from %s: incorrect number of components", namespaceStr)
+ return nil, fmt.Errorf("unable to create language namespace from %s: incorrect number of components", strings.Join(components, ":"))
}
if components[1] != ID {
- return nil, fmt.Errorf("unable to create language namespace from %s: type %s is incorrect", namespaceStr, components[1])
+ return nil, fmt.Errorf("unable to create language namespace from %s: type %s is incorrect", strings.Join(components, ":"), components[1])
}
packageType := ""
diff --git a/grype/db/v5/namespace/language/namespace_test.go b/grype/db/v5/namespace/language/namespace_test.go
index 35cd74241b7..faad7bd5d12 100644
--- a/grype/db/v5/namespace/language/namespace_test.go
+++ b/grype/db/v5/namespace/language/namespace_test.go
@@ -25,6 +25,10 @@ func TestFromString(t *testing.T) {
namespaceString: "github:language:java",
result: NewNamespace("github", syftPkg.Java, ""),
},
+ {
+ namespaceString: "github:language:rust",
+ result: NewNamespace("github", syftPkg.Rust, ""),
+ },
{
namespaceString: "abc.xyz:language:something",
result: NewNamespace("abc.xyz", syftPkg.Language("something"), ""),
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/v5/pkg/resolver/from_language.go b/grype/db/v5/pkg/resolver/from_language.go
deleted file mode 100644
index ef200c95bbf..00000000000
--- a/grype/db/v5/pkg/resolver/from_language.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package resolver
-
-import (
- "github.com/anchore/grype/grype/db/v5/pkg/resolver/java"
- "github.com/anchore/grype/grype/db/v5/pkg/resolver/python"
- "github.com/anchore/grype/grype/db/v5/pkg/resolver/stock"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-func FromLanguage(language syftPkg.Language) (Resolver, error) {
- var r Resolver
-
- switch language {
- case syftPkg.Python:
- r = &python.Resolver{}
- case syftPkg.Java:
- r = &java.Resolver{}
- default:
- r = &stock.Resolver{}
- }
-
- return r, nil
-}
diff --git a/grype/db/v5/pkg/resolver/java/resolver_test.go b/grype/db/v5/pkg/resolver/java/resolver_test.go
index ade2c8a9e15..6a4725e7f2c 100644
--- a/grype/db/v5/pkg/resolver/java/resolver_test.go
+++ b/grype/db/v5/pkg/resolver/java/resolver_test.go
@@ -11,36 +11,38 @@ import (
func TestResolver_Normalize(t *testing.T) {
tests := []struct {
- packageName string
- normalized string
+ name string
+ normalized string
}{
{
- packageName: "PyYAML",
- normalized: "pyyaml",
+ name: "PyYAML",
+ normalized: "pyyaml",
},
{
- packageName: "oslo.concurrency",
- normalized: "oslo.concurrency",
+ name: "oslo.concurrency",
+ normalized: "oslo.concurrency",
},
{
- packageName: "",
- normalized: "",
+ name: "",
+ normalized: "",
},
{
- packageName: "test---1",
- normalized: "test---1",
+ name: "test---1",
+ normalized: "test---1",
},
{
- packageName: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
- normalized: "abcd.-__.--.-___.__.--1234____----....xyzzz",
+ name: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
+ normalized: "abcd.-__.--.-___.__.--1234____----....xyzzz",
},
}
resolver := Resolver{}
for _, test := range tests {
- resolvedNames := resolver.Normalize(test.packageName)
- assert.Equal(t, resolvedNames, test.normalized)
+ t.Run(test.name, func(t *testing.T) {
+ resolvedNames := resolver.Normalize(test.name)
+ assert.Equal(t, resolvedNames, test.normalized)
+ })
}
}
@@ -53,10 +55,9 @@ func TestResolver_Resolve(t *testing.T) {
{
name: "both artifact and manifest 1",
pkg: grypePkg.Package{
- Name: "ABCD",
- Version: "1.2.3.4",
- Language: "java",
- MetadataType: "",
+ Name: "ABCD",
+ Version: "1.2.3.4",
+ Language: "java",
Metadata: grypePkg.JavaMetadata{
VirtualPath: "virtual-path-info",
PomArtifactID: "pom-ARTIFACT-ID-info",
diff --git a/grype/db/v5/pkg/resolver/python/resolver_test.go b/grype/db/v5/pkg/resolver/python/resolver_test.go
index f54aef42d0b..1d7c491dd86 100644
--- a/grype/db/v5/pkg/resolver/python/resolver_test.go
+++ b/grype/db/v5/pkg/resolver/python/resolver_test.go
@@ -8,35 +8,37 @@ import (
func TestResolver_Normalize(t *testing.T) {
tests := []struct {
- packageName string
- normalized string
+ name string
+ normalized string
}{
{
- packageName: "PyYAML",
- normalized: "pyyaml",
+ name: "PyYAML",
+ normalized: "pyyaml",
},
{
- packageName: "oslo.concurrency",
- normalized: "oslo-concurrency",
+ name: "oslo.concurrency",
+ normalized: "oslo-concurrency",
},
{
- packageName: "",
- normalized: "",
+ name: "",
+ normalized: "",
},
{
- packageName: "test---1",
- normalized: "test-1",
+ name: "test---1",
+ normalized: "test-1",
},
{
- packageName: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
- normalized: "abcd-1234-xyzzz",
+ name: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
+ normalized: "abcd-1234-xyzzz",
},
}
resolver := Resolver{}
for _, test := range tests {
- resolvedNames := resolver.Normalize(test.packageName)
- assert.Equal(t, resolvedNames, test.normalized)
+ t.Run(test.name, func(t *testing.T) {
+ resolvedNames := resolver.Normalize(test.name)
+ assert.Equal(t, resolvedNames, test.normalized)
+ })
}
}
diff --git a/grype/db/v5/pkg/resolver/resolver.go b/grype/db/v5/pkg/resolver/resolver.go
index bc253a253a9..e2b211173ce 100644
--- a/grype/db/v5/pkg/resolver/resolver.go
+++ b/grype/db/v5/pkg/resolver/resolver.go
@@ -1,10 +1,41 @@
package resolver
import (
+ "github.com/anchore/grype/grype/db/v5/pkg/resolver/java"
+ "github.com/anchore/grype/grype/db/v5/pkg/resolver/python"
+ "github.com/anchore/grype/grype/db/v5/pkg/resolver/stock"
grypePkg "github.com/anchore/grype/grype/pkg"
+ syftPkg "github.com/anchore/syft/syft/pkg"
)
type Resolver interface {
Normalize(string) string
Resolve(p grypePkg.Package) []string
}
+
+func FromLanguage(language syftPkg.Language) (Resolver, error) {
+ var r Resolver
+
+ switch language {
+ case syftPkg.Python:
+ r = &python.Resolver{}
+ case syftPkg.Java:
+ r = &java.Resolver{}
+ default:
+ r = &stock.Resolver{}
+ }
+
+ return r, nil
+}
+
+func PackageNames(p grypePkg.Package) []string {
+ names := []string{p.Name}
+ r, _ := FromLanguage(p.Language)
+ if r != nil {
+ parts := r.Resolve(p)
+ if len(parts) > 0 {
+ names = parts
+ }
+ }
+ return names
+}
diff --git a/grype/db/v5/pkg/resolver/from_language_test.go b/grype/db/v5/pkg/resolver/resolver_test.go
similarity index 100%
rename from grype/db/v5/pkg/resolver/from_language_test.go
rename to grype/db/v5/pkg/resolver/resolver_test.go
diff --git a/grype/store/store.go b/grype/db/v5/provider_store.go
similarity index 69%
rename from grype/store/store.go
rename to grype/db/v5/provider_store.go
index b7c1431960d..73457676914 100644
--- a/grype/store/store.go
+++ b/grype/db/v5/provider_store.go
@@ -1,12 +1,11 @@
-package store
+package v5
import (
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/vulnerability"
)
-type Store struct {
+type ProviderStore struct {
vulnerability.Provider
- vulnerability.MetadataProvider
match.ExclusionProvider
}
diff --git a/grype/db/v5/store.go b/grype/db/v5/store.go
index f5ca52068d7..fcb983cd9fe 100644
--- a/grype/db/v5/store.go
+++ b/grype/db/v5/store.go
@@ -1,9 +1,10 @@
package v5
+import "io"
+
type Store interface {
StoreReader
StoreWriter
- DBCloser
}
type StoreReader interface {
@@ -12,6 +13,7 @@ type StoreReader interface {
VulnerabilityStoreReader
VulnerabilityMetadataStoreReader
VulnerabilityMatchExclusionStoreReader
+ io.Closer
}
type StoreWriter interface {
@@ -19,12 +21,9 @@ type StoreWriter interface {
VulnerabilityStoreWriter
VulnerabilityMetadataStoreWriter
VulnerabilityMatchExclusionStoreWriter
+ io.Closer
}
type DiffReader interface {
DiffStore(s StoreReader) (*[]Diff, error)
}
-
-type DBCloser interface {
- Close()
-}
diff --git a/grype/db/v5/store/store.go b/grype/db/v5/store/store.go
index e1505ce3e1d..8b282f76ed2 100644
--- a/grype/db/v5/store/store.go
+++ b/grype/db/v5/store/store.go
@@ -4,6 +4,7 @@ import (
"fmt"
"sort"
+ _ "github.com/glebarez/sqlite" // provide the sqlite dialect to gorm via import
"github.com/go-test/deep"
"gorm.io/gorm"
@@ -11,7 +12,6 @@ import (
v5 "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/db/v5/store/model"
"github.com/anchore/grype/internal/stringutil"
- _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
)
// store holds an instance of the database connection
@@ -19,31 +19,22 @@ type store struct {
db *gorm.DB
}
+func models() []any {
+ return []any{
+ model.IDModel{},
+ model.VulnerabilityModel{},
+ model.VulnerabilityMetadataModel{},
+ model.VulnerabilityMatchExclusionModel{},
+ }
+}
+
// New creates a new instance of the store.
func New(dbFilePath string, overwrite bool) (v5.Store, error) {
- db, err := gormadapter.Open(dbFilePath, overwrite)
+ db, err := gormadapter.Open(dbFilePath, gormadapter.WithTruncate(overwrite, models(), nil))
if err != nil {
return nil, err
}
- if overwrite {
- // TODO: automigrate could write to the database,
- // we should be validating the database is the correct database based on the version in the ID table before
- // automigrating
- if err := db.AutoMigrate(&model.IDModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate ID model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityMetadataModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability Metadata model: %w", err)
- }
- if err := db.AutoMigrate(&model.VulnerabilityMatchExclusionModel{}); err != nil {
- return nil, fmt.Errorf("unable to migrate Vulnerability Match Exclusion model: %w", err)
- }
- }
-
return &store{
db: db,
}, nil
@@ -99,7 +90,13 @@ func (s *store) GetVulnerabilityNamespaces() ([]string, error) {
func (s *store) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) {
var models []model.VulnerabilityModel
- result := s.db.Where("namespace = ? AND id = ?", namespace, id).Find(&models)
+ query := s.db.Where("id = ?", id)
+
+ if namespace != "" {
+ query = query.Where("namespace = ?", namespace)
+ }
+
+ result := query.Find(&models)
var vulnerabilities = make([]v5.Vulnerability, len(models))
for idx, m := range models {
@@ -280,13 +277,15 @@ func (s *store) AddVulnerabilityMatchExclusion(exclusions ...v5.VulnerabilityMat
return nil
}
-func (s *store) Close() {
+func (s *store) Close() error {
s.db.Exec("VACUUM;")
- sqlDB, err := s.db.DB()
- if err != nil {
+ sqlDB, _ := s.db.DB()
+ if sqlDB != nil {
_ = sqlDB.Close()
}
+
+ return nil
}
// GetAllVulnerabilities gets all vulnerabilities in the database
diff --git a/grype/db/v5/vulnerability.go b/grype/db/v5/vulnerability.go
index cba94b494cc..49ac9e014d3 100644
--- a/grype/db/v5/vulnerability.go
+++ b/grype/db/v5/vulnerability.go
@@ -1,10 +1,16 @@
package v5
import (
+ "fmt"
"sort"
"strings"
- "github.com/anchore/grype/grype/db/v5/pkg/qualifier"
+ qualifierV5 "github.com/anchore/grype/grype/db/v5/pkg/qualifier"
+ "github.com/anchore/grype/grype/pkg/qualifier"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/syft/syft/cpe"
)
// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.
@@ -12,7 +18,7 @@ type Vulnerability struct {
ID string `json:"id"` // The identifier of the vulnerability or advisory
PackageName string `json:"package_name"` // The name of the package that is vulnerable
Namespace string `json:"namespace"` // The ecosystem where the package resides
- PackageQualifiers []qualifier.Qualifier `json:"package_qualifiers"` // The qualifiers for determining if a package is vulnerable
+ PackageQualifiers []qualifierV5.Qualifier `json:"package_qualifiers"` // The qualifiers for determining if a package is vulnerable
VersionConstraint string `json:"version_constraint"` // The version range which the given package is vulnerable
VersionFormat string `json:"version_format"` // The format which all version fields should be interpreted as
CPEs []string `json:"cpes"` // The CPEs which are considered vulnerable
@@ -106,9 +112,66 @@ func sortAdvisories(advisories []Advisory) []Advisory {
return advisories
}
-func sortPackageQualifiers(qualifiers []qualifier.Qualifier) []qualifier.Qualifier {
+func sortPackageQualifiers(qualifiers []qualifierV5.Qualifier) []qualifierV5.Qualifier {
sort.SliceStable(qualifiers, func(i, j int) bool {
return qualifiers[i].String() < qualifiers[j].String()
})
return qualifiers
}
+
+func NewVulnerability(vuln Vulnerability) (*vulnerability.Vulnerability, error) {
+ format := version.ParseFormat(vuln.VersionFormat)
+
+ constraint, err := version.GetConstraint(vuln.VersionConstraint, format)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse constraint='%s' format='%s': %w", vuln.VersionConstraint, format, err)
+ }
+
+ pkgQualifiers := make([]qualifier.Qualifier, len(vuln.PackageQualifiers))
+ for idx, q := range vuln.PackageQualifiers {
+ pkgQualifiers[idx] = q.Parse()
+ }
+
+ advisories := make([]vulnerability.Advisory, len(vuln.Advisories))
+ for idx, advisory := range vuln.Advisories {
+ advisories[idx] = vulnerability.Advisory{
+ ID: advisory.ID,
+ Link: advisory.Link,
+ }
+ }
+
+ var relatedVulnerabilities []vulnerability.Reference
+ for _, r := range vuln.RelatedVulnerabilities {
+ relatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{
+ ID: r.ID,
+ Namespace: r.Namespace,
+ })
+ }
+
+ var cpes []cpe.CPE
+ for _, cp := range vuln.CPEs {
+ c, err := cpe.New(cp, "")
+ if err != nil {
+ log.WithFields("err", err, "cpe", cp).Debug("failed to parse CPE")
+ continue
+ }
+ cpes = append(cpes, c)
+ }
+
+ return &vulnerability.Vulnerability{
+ PackageName: vuln.PackageName,
+ Constraint: constraint,
+ Reference: vulnerability.Reference{
+ ID: vuln.ID,
+ Namespace: vuln.Namespace,
+ },
+ CPEs: cpes,
+ PackageQualifiers: pkgQualifiers,
+ Fix: vulnerability.Fix{
+ Versions: vuln.Fix.Versions,
+ State: vulnerability.FixState(vuln.Fix.State),
+ },
+ Advisories: advisories,
+ RelatedVulnerabilities: relatedVulnerabilities,
+ }, nil
+}
diff --git a/grype/db/v5/vulnerability_metadata.go b/grype/db/v5/vulnerability_metadata.go
index e37395cfd97..b7fe1fce1e5 100644
--- a/grype/db/v5/vulnerability_metadata.go
+++ b/grype/db/v5/vulnerability_metadata.go
@@ -1,6 +1,10 @@
package v5
-import "reflect"
+import (
+ "reflect"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
// VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching.
type VulnerabilityMetadata struct {
@@ -76,3 +80,18 @@ func (v *VulnerabilityMetadata) Equal(vv VulnerabilityMetadata) bool {
return true
}
+
+func NewMetadata(m *VulnerabilityMetadata) (*vulnerability.Metadata, error) {
+ if m == nil {
+ return nil, nil
+ }
+ return &vulnerability.Metadata{
+ ID: m.ID,
+ DataSource: m.DataSource,
+ Namespace: m.Namespace,
+ Severity: m.Severity,
+ URLs: m.URLs,
+ Description: m.Description,
+ Cvss: NewCvss(m.Cvss),
+ }, nil
+}
diff --git a/grype/db/v6/affected_cpe_store.go b/grype/db/v6/affected_cpe_store.go
new file mode 100644
index 00000000000..98a92a81550
--- /dev/null
+++ b/grype/db/v6/affected_cpe_store.go
@@ -0,0 +1,226 @@
+package v6
+
+import (
+ "fmt"
+ "time"
+
+ "gorm.io/gorm"
+
+ "github.com/anchore/go-logger"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/syft/syft/cpe"
+)
+
+type AffectedCPEStoreWriter interface {
+ AddAffectedCPEs(packages ...*AffectedCPEHandle) error
+}
+
+type AffectedCPEStoreReader interface {
+ GetAffectedCPEs(cpe *cpe.Attributes, config *GetAffectedCPEOptions) ([]AffectedCPEHandle, error)
+}
+
+type GetAffectedCPEOptions struct {
+ PreloadCPE bool
+ PreloadVulnerability bool
+ PreloadBlob bool
+ Vulnerabilities []VulnerabilitySpecifier
+ AllowBroadCPEMatching bool
+ Limit int
+}
+
+type affectedCPEStore struct {
+ db *gorm.DB
+ blobStore *blobStore
+}
+
+func newAffectedCPEStore(db *gorm.DB, bs *blobStore) *affectedCPEStore {
+ return &affectedCPEStore{
+ db: db,
+ blobStore: bs,
+ }
+}
+
+// AddAffectedCPEs adds one or more affected CPEs to the store
+func (s *affectedCPEStore) AddAffectedCPEs(packages ...*AffectedCPEHandle) error {
+ if err := s.addCpes(packages...); err != nil {
+ return fmt.Errorf("unable to add CPEs from affected package CPEs: %w", err)
+ }
+ for _, pkg := range packages {
+ if err := s.blobStore.addBlobable(pkg); err != nil {
+ return fmt.Errorf("unable to add affected package blob: %w", err)
+ }
+
+ if err := s.db.Omit("CPE").Create(pkg).Error; err != nil {
+ return fmt.Errorf("unable to add affected CPEs: %w", err)
+ }
+ }
+ return nil
+}
+
+func (s *affectedCPEStore) addCpes(packages ...*AffectedCPEHandle) error { // nolint:dupl
+ cacheInst, ok := cacheFromContext(s.db.Statement.Context)
+ if !ok {
+ return fmt.Errorf("unable to fetch CPE cache from context")
+ }
+
+ var final []*Cpe
+ byCacheKey := make(map[string][]*Cpe)
+ for _, p := range packages {
+ if p.CPE != nil {
+ key := p.CPE.cacheKey()
+ if existingID, ok := cacheInst.getID(p.CPE); ok {
+ // seen in a previous transaction...
+ p.CpeID = existingID
+ } else if _, ok := byCacheKey[key]; !ok {
+ // not seen within this transaction
+ final = append(final, p.CPE)
+ }
+ byCacheKey[key] = append(byCacheKey[key], p.CPE)
+ }
+ }
+
+ if len(final) == 0 {
+ return nil
+ }
+
+ if err := s.db.Create(final).Error; err != nil {
+ return fmt.Errorf("unable to create CPE records: %w", err)
+ }
+
+ // update the cache with the new records
+ for _, ref := range final {
+ cacheInst.set(ref)
+ }
+
+ // update all references with the IDs from the cache
+ for _, refs := range byCacheKey {
+ for _, ref := range refs {
+ id, ok := cacheInst.getID(ref)
+ if ok {
+ ref.setRowID(id)
+ }
+ }
+ }
+
+ // update the parent objects with the FK ID
+ for _, p := range packages {
+ if p.CPE != nil {
+ p.CpeID = p.CPE.ID
+ }
+ }
+ return nil
+}
+
+// GetAffectedCPEs retrieves a single AffectedCPEHandle by one or more CPE fields (not including version and update fields, which are ignored)
+func (s *affectedCPEStore) GetAffectedCPEs(cpe *cpe.Attributes, config *GetAffectedCPEOptions) ([]AffectedCPEHandle, error) {
+ if config == nil {
+ config = &GetAffectedCPEOptions{}
+ }
+
+ fields := make(logger.Fields)
+ count := 0
+ if cpe == nil {
+ fields["cpe"] = "any"
+ } else {
+ fields["cpe"] = cpe.String()
+ }
+ start := time.Now()
+ defer func() {
+ fields["duration"] = time.Since(start)
+ log.WithFields(fields).Trace("fetched affected CPE record")
+ }()
+
+ query := s.handleCPE(s.db, cpe, config.AllowBroadCPEMatching)
+
+ var err error
+ query, err = s.handleVulnerabilityOptions(query, config.Vulnerabilities)
+ if err != nil {
+ return nil, err
+ }
+
+ query = s.handlePreload(query, *config)
+
+ var models []AffectedCPEHandle
+
+ var results []*AffectedCPEHandle
+ if err := query.FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error { // nolint:dupl
+ if config.PreloadBlob {
+ var blobs []blobable
+ for _, r := range results {
+ blobs = append(blobs, r)
+ }
+ if err := s.blobStore.attachBlobValue(blobs...); err != nil {
+ return fmt.Errorf("unable to attach blobs: %w", err)
+ }
+ }
+
+ if config.PreloadVulnerability {
+ var vulns []blobable
+ for _, r := range results {
+ if r.Vulnerability != nil {
+ vulns = append(vulns, r.Vulnerability)
+ }
+ }
+ if err := s.blobStore.attachBlobValue(vulns...); err != nil {
+ return fmt.Errorf("unable to attach vulnerability blob: %w", err)
+ }
+ }
+
+ for _, r := range results {
+ models = append(models, *r)
+ }
+
+ count += len(results)
+
+ if config.Limit > 0 && len(models) >= config.Limit {
+ return ErrLimitReached
+ }
+
+ return nil
+ }).Error; err != nil {
+ return models, fmt.Errorf("unable to fetch affected CPE records: %w", err)
+ }
+
+ fields["records"] = count
+
+ return models, nil
+}
+
+func (s *affectedCPEStore) handleCPE(query *gorm.DB, c *cpe.Attributes, allowBroad bool) *gorm.DB {
+ if c == nil {
+ return query
+ }
+ query = query.Joins("JOIN cpes ON cpes.id = affected_cpe_handles.cpe_id")
+
+ return handleCPEOptions(query, c, allowBroad)
+}
+
+func (s *affectedCPEStore) handleVulnerabilityOptions(query *gorm.DB, configs []VulnerabilitySpecifier) (*gorm.DB, error) {
+ if len(configs) == 0 {
+ return query, nil
+ }
+
+ query = query.Joins("JOIN vulnerability_handles ON affected_cpe_handles.vulnerability_id = vulnerability_handles.id")
+
+ return handleVulnerabilityOptions(s.db, query, configs...)
+}
+
+func (s *affectedCPEStore) handlePreload(query *gorm.DB, config GetAffectedCPEOptions) *gorm.DB {
+ var limitArgs []interface{}
+ if config.Limit > 0 {
+ query = query.Limit(config.Limit)
+ limitArgs = append(limitArgs, func(db *gorm.DB) *gorm.DB {
+ return db.Limit(config.Limit)
+ })
+ }
+
+ if config.PreloadCPE {
+ query = query.Preload("CPE", limitArgs...)
+ }
+
+ if config.PreloadVulnerability {
+ query = query.Preload("Vulnerability", limitArgs...).Preload("Vulnerability.Provider", limitArgs...)
+ }
+
+ return query
+}
diff --git a/grype/db/v6/affected_cpe_store_test.go b/grype/db/v6/affected_cpe_store_test.go
new file mode 100644
index 00000000000..41414749bd3
--- /dev/null
+++ b/grype/db/v6/affected_cpe_store_test.go
@@ -0,0 +1,256 @@
+package v6
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/syft/syft/cpe"
+)
+
+func TestAffectedCPEStore_AddAffectedCPEs(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newAffectedCPEStore(db, bw)
+
+ cpe1 := &AffectedCPEHandle{
+ Vulnerability: &VulnerabilityHandle{ // vuln id = 1
+ Provider: &Provider{
+ ID: "nvd",
+ },
+ Name: "CVE-2023-5678",
+ },
+ CPE: &Cpe{
+ Part: "a",
+ Vendor: "vendor-1",
+ Product: "product-1",
+ Edition: "edition-1",
+ },
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-5678"},
+ },
+ }
+
+ cpe2 := testAffectedCPEHandle() // vuln id = 2
+
+ err := s.AddAffectedCPEs(cpe1, cpe2)
+ require.NoError(t, err)
+
+ var result1 AffectedCPEHandle
+ err = db.Where("cpe_id = ?", 1).First(&result1).Error
+ require.NoError(t, err)
+ assert.Equal(t, cpe1.VulnerabilityID, result1.VulnerabilityID)
+ assert.Equal(t, cpe1.ID, result1.ID)
+ assert.Equal(t, cpe1.BlobID, result1.BlobID)
+ assert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch
+
+ var result2 AffectedCPEHandle
+ err = db.Where("cpe_id = ?", 2).First(&result2).Error
+ require.NoError(t, err)
+ assert.Equal(t, cpe2.VulnerabilityID, result2.VulnerabilityID)
+ assert.Equal(t, cpe2.ID, result2.ID)
+ assert.Equal(t, cpe2.BlobID, result2.BlobID)
+ assert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch
+}
+
+func TestAffectedCPEStore_GetCPEs(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newAffectedCPEStore(db, bw)
+
+ c := testAffectedCPEHandle()
+ err := s.AddAffectedCPEs(c)
+ require.NoError(t, err)
+
+ results, err := s.GetAffectedCPEs(cpeFromProduct(c.CPE.Product), nil)
+ require.NoError(t, err)
+
+ expected := []AffectedCPEHandle{*c}
+ require.Len(t, results, len(expected))
+ result := results[0]
+ assert.Equal(t, c.CpeID, result.CpeID)
+ assert.Equal(t, c.ID, result.ID)
+ assert.Equal(t, c.BlobID, result.BlobID)
+ require.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch
+
+ // fetch again with blob & cpe preloaded
+ results, err = s.GetAffectedCPEs(cpeFromProduct(c.CPE.Product), &GetAffectedCPEOptions{PreloadCPE: true, PreloadBlob: true, PreloadVulnerability: true})
+ require.NoError(t, err)
+ require.Len(t, results, len(expected))
+ result = results[0]
+ assert.NotNil(t, result.BlobValue)
+ if d := cmp.Diff(*c, result); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+}
+
+func TestAffectedCPEStore_GetExact(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newAffectedCPEStore(db, bw)
+
+ c := testAffectedCPEHandle()
+ err := s.AddAffectedCPEs(c)
+ require.NoError(t, err)
+
+ // we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses)
+ results, err := s.GetAffectedCPEs(toCPE(c.CPE), nil)
+ require.NoError(t, err)
+
+ expected := []AffectedCPEHandle{*c}
+ require.Len(t, results, len(expected))
+ result := results[0]
+ assert.Equal(t, c.CpeID, result.CpeID)
+
+}
+
+func TestAffectedCPEStore_Get_CaseInsensitive(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newAffectedCPEStore(db, bw)
+
+ c := testAffectedCPEHandle()
+ err := s.AddAffectedCPEs(c)
+ require.NoError(t, err)
+
+ // we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses)
+ results, err := s.GetAffectedCPEs(toCPE(&Cpe{
+ Part: "Application", // capitalized
+ Vendor: "Vendor", // capitalized
+ Product: "Product", // capitalized
+ Edition: "Edition", // capitalized
+ Language: "Language", // capitalized
+ SoftwareEdition: "Software_edition", // capitalized
+ TargetHardware: "Target_hardware", // capitalized
+ TargetSoftware: "Target_software", // capitalized
+ Other: "Other", // capitalized
+ }), nil)
+ require.NoError(t, err)
+
+ expected := []AffectedCPEHandle{*c}
+ require.Len(t, results, len(expected))
+ result := results[0]
+ assert.Equal(t, c.CpeID, result.CpeID)
+}
+
+func TestAffectedCPEStore_PreventDuplicateCPEs(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newAffectedCPEStore(db, bw)
+
+ cpe1 := &AffectedCPEHandle{
+ Vulnerability: &VulnerabilityHandle{ // vuln id = 1
+ Name: "CVE-2023-5678",
+ Provider: &Provider{
+ ID: "nvd",
+ },
+ },
+ CPE: &Cpe{ // ID = 1
+ Part: "a",
+ Vendor: "vendor-1",
+ Product: "product-1",
+ Edition: "edition-1",
+ },
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-5678"},
+ },
+ }
+
+ err := s.AddAffectedCPEs(cpe1)
+ require.NoError(t, err)
+
+ // attempt to add a duplicate CPE with the same values
+ duplicateCPE := &AffectedCPEHandle{
+ Vulnerability: &VulnerabilityHandle{ // vuln id = 2, different VulnerabilityID for testing...
+ Name: "CVE-2024-1234",
+ Provider: &Provider{
+ ID: "nvd",
+ },
+ },
+ CpeID: 2, // for testing explicitly set to 2, but this is unrealistic
+ CPE: &Cpe{
+ ID: 2, // different, again, unrealistic but useful for testing
+ Part: "a", // same
+ Vendor: "vendor-1", // same
+ Product: "product-1", // same
+ Edition: "edition-1", // same
+ },
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2024-1234"},
+ },
+ }
+
+ err = s.AddAffectedCPEs(duplicateCPE)
+ require.NoError(t, err)
+
+ require.Equal(t, cpe1.CpeID, duplicateCPE.CpeID, "expected the CPE DB ID to be the same")
+
+ var existingCPEs []Cpe
+ err = db.Find(&existingCPEs).Error
+ require.NoError(t, err)
+ require.Len(t, existingCPEs, 1, "expected only one CPE to exist")
+
+ actualHandles, err := s.GetAffectedCPEs(cpeFromProduct(cpe1.CPE.Product), &GetAffectedCPEOptions{
+ PreloadCPE: true,
+ PreloadBlob: true,
+ PreloadVulnerability: true,
+ })
+ require.NoError(t, err)
+
+ // the CPEs should be the same, and the store should reconcile the IDs
+ duplicateCPE.CpeID = cpe1.CpeID
+ duplicateCPE.CPE.ID = cpe1.CPE.ID
+
+ expected := []AffectedCPEHandle{*cpe1, *duplicateCPE}
+ require.Len(t, actualHandles, len(expected), "expected both handles to be stored")
+ if d := cmp.Diff(expected, actualHandles); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+}
+
+func cpeFromProduct(product string) *cpe.Attributes {
+ return &cpe.Attributes{
+ Product: product,
+ }
+}
+
+func toCPE(c *Cpe) *cpe.Attributes {
+ return &cpe.Attributes{
+ Part: c.Part,
+ Vendor: c.Vendor,
+ Product: c.Product,
+ Edition: c.Edition,
+ Language: c.Language,
+ SWEdition: c.SoftwareEdition,
+ TargetSW: c.TargetSoftware,
+ TargetHW: c.TargetHardware,
+ Other: c.Other,
+ }
+}
+
+func testAffectedCPEHandle() *AffectedCPEHandle {
+ return &AffectedCPEHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2024-4321",
+ Provider: &Provider{
+ ID: "nvd",
+ },
+ },
+ CPE: &Cpe{
+ Part: "application",
+ Vendor: "vendor",
+ Product: "product",
+ Edition: "edition",
+ Language: "language",
+ SoftwareEdition: "software_edition",
+ TargetHardware: "target_hardware",
+ TargetSoftware: "target_software",
+ Other: "other",
+ },
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2024-4321"},
+ },
+ }
+}
diff --git a/grype/db/v6/affected_package_store.go b/grype/db/v6/affected_package_store.go
new file mode 100644
index 00000000000..59ef9da44b5
--- /dev/null
+++ b/grype/db/v6/affected_package_store.go
@@ -0,0 +1,766 @@
+package v6
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strings"
+ "time"
+
+ "golang.org/x/exp/maps"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/syft/syft/cpe"
+)
+
+const (
+ anyPkg = "any"
+ anyOS = "any"
+)
+
+var NoOSSpecified = &OSSpecifier{}
+var AnyOSSpecified *OSSpecifier
+var AnyPackageSpecified *PackageSpecifier
+var ErrMissingOSIdentification = errors.New("missing OS name or codename")
+var ErrOSNotPresent = errors.New("OS not present")
+var ErrLimitReached = errors.New("query limit reached")
+
+type GetAffectedPackageOptions struct {
+ PreloadOS bool
+ PreloadPackage bool
+ PreloadPackageCPEs bool
+ PreloadVulnerability bool
+ PreloadBlob bool
+ OSs OSSpecifiers
+ Vulnerabilities VulnerabilitySpecifiers
+ AllowBroadCPEMatching bool
+ Limit int
+}
+
+type PackageSpecifiers []*PackageSpecifier
+
+type PackageSpecifier struct {
+ Name string
+ Ecosystem string
+ CPE *cpe.Attributes
+}
+
+func (p *PackageSpecifier) String() string {
+ if p == nil {
+ return anyPkg
+ }
+
+ var args []string
+ if p.Name != "" {
+ args = append(args, fmt.Sprintf("name=%s", p.Name))
+ }
+
+ if p.Ecosystem != "" {
+ args = append(args, fmt.Sprintf("ecosystem=%s", p.Ecosystem))
+ }
+
+ if p.CPE != nil {
+ args = append(args, fmt.Sprintf("cpe=%s", p.CPE.String()))
+ }
+
+ if len(args) > 0 {
+ return fmt.Sprintf("package(%s)", strings.Join(args, ", "))
+ }
+
+ return anyPkg
+}
+
+func (p PackageSpecifiers) String() string {
+ if len(p) == 0 {
+ return anyPkg
+ }
+
+ var parts []string
+ for _, v := range p {
+ parts = append(parts, v.String())
+ }
+ return strings.Join(parts, ", ")
+}
+
+type OSSpecifiers []*OSSpecifier
+
+// OSSpecifier is a struct that represents a distro in a way that can be used to query the affected package store.
+type OSSpecifier struct {
+ // Name of the distro as identified by the ID field in /etc/os-release (or similar normalized name, e.g. "oracle" instead of "ol")
+ Name string
+
+ // MajorVersion is the first field in the VERSION_ID field in /etc/os-release (e.g. 7 in "7.0.1406")
+ MajorVersion string
+
+ // 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
+}
+
+func (d *OSSpecifier) String() string {
+ if d == nil {
+ return anyOS
+ }
+
+ if *d == *NoOSSpecified {
+ return "none"
+ }
+
+ var version string
+ if d.MajorVersion != "" {
+ version = d.version()
+ } else {
+ version = d.LabelVersion
+ }
+
+ distroDisplayName := d.Name
+ if version != "" {
+ distroDisplayName += "@" + version
+ }
+ if version == d.MajorVersion && d.LabelVersion != "" {
+ distroDisplayName += " (" + d.LabelVersion + ")"
+ }
+
+ return distroDisplayName
+}
+
+func (d OSSpecifier) version() string {
+ if d.MajorVersion != "" {
+ if d.MinorVersion != "" {
+ if d.RemainingVersion != "" {
+ return d.MajorVersion + "." + d.MinorVersion + "." + d.RemainingVersion
+ }
+ return d.MajorVersion + "." + d.MinorVersion
+ }
+ return d.MajorVersion
+ }
+
+ return d.LabelVersion
+}
+
+func (d OSSpecifiers) String() string {
+ if d.IsAny() {
+ return anyOS
+ }
+ var parts []string
+ for _, v := range d {
+ parts = append(parts, v.String())
+ }
+ return strings.Join(parts, ", ")
+}
+
+func (d OSSpecifiers) IsAny() bool {
+ if len(d) == 0 {
+ return true
+ }
+ if len(d) == 1 && d[0] == AnyOSSpecified {
+ return true
+ }
+ return false
+}
+
+func (d OSSpecifier) matchesVersionPattern(pattern string) bool {
+ // check if version or version label matches the given regex
+ r, err := regexp.Compile(pattern)
+ if err != nil {
+ log.Tracef("failed to compile distro specifier regex pattern %q: %v", pattern, err)
+ return false
+ }
+
+ if r.MatchString(d.version()) {
+ return true
+ }
+
+ if d.LabelVersion != "" {
+ return r.MatchString(d.LabelVersion)
+ }
+ return false
+}
+
+type AffectedPackageStoreWriter interface {
+ AddAffectedPackages(packages ...*AffectedPackageHandle) error
+}
+
+type AffectedPackageStoreReader interface {
+ GetAffectedPackages(pkg *PackageSpecifier, config *GetAffectedPackageOptions) ([]AffectedPackageHandle, error)
+}
+
+type affectedPackageStore struct {
+ db *gorm.DB
+ blobStore *blobStore
+}
+
+func newAffectedPackageStore(db *gorm.DB, bs *blobStore) *affectedPackageStore {
+ return &affectedPackageStore{
+ db: db,
+ blobStore: bs,
+ }
+}
+
+func (s *affectedPackageStore) AddAffectedPackages(packages ...*AffectedPackageHandle) error {
+ omit := []string{"OperatingSystem"}
+ if err := s.addOs(packages...); err != nil {
+ return fmt.Errorf("unable to add affected package OS: %w", err)
+ }
+
+ hasCpes, err := s.addPackages(packages...)
+ if err != nil {
+ return fmt.Errorf("unable to add affected packages: %w", err)
+ }
+
+ if !hasCpes {
+ omit = append(omit, "Package")
+ }
+
+ for _, v := range packages {
+ if err := s.blobStore.addBlobable(v); err != nil {
+ return fmt.Errorf("unable to add affected blob: %w", err)
+ }
+
+ if err := s.db.Omit(omit...).Create(v).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (s *affectedPackageStore) addPackages(packages ...*AffectedPackageHandle) (bool, error) { // nolint:dupl
+ cacheInst, ok := cacheFromContext(s.db.Statement.Context)
+ if !ok {
+ return false, fmt.Errorf("unable to fetch package cache from context")
+ }
+ var final []*Package
+ var hasCPEs bool
+ byCacheKey := make(map[string][]*Package)
+ for _, p := range packages {
+ if p.Package != nil {
+ if len(p.Package.CPEs) > 0 {
+ // never use the cache if there are CPEs involved
+ final = append(final, p.Package)
+ hasCPEs = true
+ continue
+ }
+ key := p.Package.cacheKey()
+ if existingID, ok := cacheInst.getID(p.Package); ok {
+ // seen in a previous transaction...
+ p.PackageID = existingID
+ } else if _, ok := byCacheKey[key]; !ok {
+ // not seen within this transaction
+ final = append(final, p.Package)
+ }
+ byCacheKey[key] = append(byCacheKey[key], p.Package)
+ }
+ }
+
+ if len(final) == 0 {
+ return false, nil
+ }
+
+ // since there is risk of needing to write through packages with conflicting CPEs we cannot write these in batches,
+ // and since the before hooks reason about previous entries within this loop (potentially) we must ensure that
+ // these are written in different transactions.
+ for _, p := range final {
+ if err := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(p).Error; err != nil {
+ return false, fmt.Errorf("unable to create package records: %w", err)
+ }
+ }
+
+ // update the cache with the new records
+ for _, ref := range final {
+ cacheInst.set(ref)
+ }
+
+ // update all references with the IDs from the cache
+ for _, refs := range byCacheKey {
+ for _, ref := range refs {
+ id, ok := cacheInst.getID(ref)
+ if ok {
+ ref.setRowID(id)
+ }
+ }
+ }
+
+ // update the parent objects with the FK ID
+ for _, p := range packages {
+ if p.Package != nil {
+ p.PackageID = p.Package.ID
+ }
+ }
+ return hasCPEs, nil
+}
+
+func (s *affectedPackageStore) addOs(packages ...*AffectedPackageHandle) error { // nolint:dupl
+ cacheInst, ok := cacheFromContext(s.db.Statement.Context)
+ if !ok {
+ return fmt.Errorf("unable to fetch OS cache from context")
+ }
+
+ var final []*OperatingSystem
+ byCacheKey := make(map[string][]*OperatingSystem)
+ for _, p := range packages {
+ if p.OperatingSystem != nil {
+ p.OperatingSystem.clean()
+ key := p.OperatingSystem.cacheKey()
+ if existingID, ok := cacheInst.getID(p.OperatingSystem); ok {
+ // seen in a previous transaction...
+ p.OperatingSystemID = &existingID
+ } else if _, ok := byCacheKey[key]; !ok {
+ // not seen within this transaction
+ final = append(final, p.OperatingSystem)
+ }
+ byCacheKey[key] = append(byCacheKey[key], p.OperatingSystem)
+ }
+ }
+
+ if len(final) == 0 {
+ return nil
+ }
+
+ if err := s.db.Create(final).Error; err != nil {
+ return fmt.Errorf("unable to create OS records: %w", err)
+ }
+
+ // update the cache with the new records
+ for _, ref := range final {
+ cacheInst.set(ref)
+ }
+
+ // update all references with the IDs from the cache
+ for _, refs := range byCacheKey {
+ for _, ref := range refs {
+ id, ok := cacheInst.getID(ref)
+ if ok {
+ ref.setRowID(id)
+ }
+ }
+ }
+
+ // update the parent objects with the FK ID
+ for _, p := range packages {
+ if p.OperatingSystem != nil {
+ p.OperatingSystemID = &p.OperatingSystem.ID
+ }
+ }
+ return nil
+}
+
+func (s *affectedPackageStore) GetAffectedPackages(pkg *PackageSpecifier, config *GetAffectedPackageOptions) ([]AffectedPackageHandle, error) { // nolint:funlen
+ if config == nil {
+ config = &GetAffectedPackageOptions{}
+ }
+
+ start := time.Now()
+ count := 0
+ defer func() {
+ log.
+ WithFields(
+ "pkg", pkg.String(),
+ "distro", config.OSs,
+ "vulns", config.Vulnerabilities,
+ "duration", time.Since(start),
+ "records", count,
+ ).
+ Trace("fetched affected package record")
+ }()
+
+ query := s.handlePackage(s.db, pkg, config.AllowBroadCPEMatching)
+
+ var err error
+ query, err = s.handleVulnerabilityOptions(query, config.Vulnerabilities)
+ if err != nil {
+ return nil, err
+ }
+
+ query, err = s.handleOSOptions(query, config.OSs)
+ if err != nil {
+ return nil, err
+ }
+
+ query = s.handlePreload(query, *config)
+
+ var models []AffectedPackageHandle
+
+ var results []*AffectedPackageHandle
+ if err := query.FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error { // nolint:dupl
+ if config.PreloadBlob {
+ var blobs []blobable
+ for _, r := range results {
+ blobs = append(blobs, r)
+ }
+ if err := s.blobStore.attachBlobValue(blobs...); err != nil {
+ return fmt.Errorf("unable to attach affected package blobs: %w", err)
+ }
+ }
+
+ if config.PreloadVulnerability {
+ var vulns []blobable
+ for _, r := range results {
+ if r.Vulnerability != nil {
+ vulns = append(vulns, r.Vulnerability)
+ }
+ }
+ if err := s.blobStore.attachBlobValue(vulns...); err != nil {
+ return fmt.Errorf("unable to attach vulnerability blob: %w", err)
+ }
+ }
+
+ for _, r := range results {
+ models = append(models, *r)
+ }
+
+ count += len(results)
+
+ if config.Limit > 0 && len(models) >= config.Limit {
+ return ErrLimitReached
+ }
+
+ return nil
+ }).Error; err != nil {
+ return models, fmt.Errorf("unable to fetch affected package records: %w", err)
+ }
+
+ return models, nil
+}
+
+func (s *affectedPackageStore) handlePackage(query *gorm.DB, p *PackageSpecifier, allowBroad bool) *gorm.DB {
+ if p == nil {
+ return query
+ }
+
+ if err := s.applyPackageAlias(p); err != nil {
+ log.Errorf("failed to apply package alias: %v", err)
+ }
+
+ query = query.Joins("JOIN packages ON affected_package_handles.package_id = packages.id")
+
+ if p.Name != "" {
+ query = query.Where("packages.name = ? collate nocase", p.Name)
+ }
+ if p.Ecosystem != "" {
+ query = query.Where("packages.ecosystem = ? collate nocase", p.Ecosystem)
+ }
+
+ if p.CPE != nil {
+ query = query.Joins("JOIN package_cpes ON packages.id = package_cpes.package_id")
+ query = query.Joins("JOIN cpes ON package_cpes.cpe_id = cpes.id")
+ query = handleCPEOptions(query, p.CPE, allowBroad)
+ }
+
+ return query
+}
+
+func (s *affectedPackageStore) applyPackageAlias(d *PackageSpecifier) error {
+ if d.Ecosystem == "" {
+ return nil
+ }
+
+ // only ecosystem replacement is supported today
+ var aliases []PackageSpecifierOverride
+ err := s.db.Where("ecosystem = ? collate nocase", d.Ecosystem).Find(&aliases).Error
+ if err != nil {
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
+ return fmt.Errorf("failed to resolve alias for distro %q: %w", d.Name, err)
+ }
+ return nil
+ }
+
+ var alias *PackageSpecifierOverride
+
+ for _, a := range aliases {
+ if a.Ecosystem == "" {
+ continue
+ }
+
+ alias = &a
+ break
+ }
+
+ if alias == nil {
+ return nil
+ }
+
+ if alias.ReplacementEcosystem != nil {
+ d.Ecosystem = *alias.ReplacementEcosystem
+ }
+
+ return nil
+}
+
+func (s *affectedPackageStore) handleVulnerabilityOptions(query *gorm.DB, configs []VulnerabilitySpecifier) (*gorm.DB, error) {
+ if len(configs) == 0 {
+ return query, nil
+ }
+ query = query.Joins("JOIN vulnerability_handles ON affected_package_handles.vulnerability_id = vulnerability_handles.id")
+
+ return handleVulnerabilityOptions(s.db, query, configs...)
+}
+
+func (s *affectedPackageStore) handleOSOptions(query *gorm.DB, configs []*OSSpecifier) (*gorm.DB, error) {
+ ids := map[int64]struct{}{}
+
+ if len(configs) == 0 {
+ configs = append(configs, AnyOSSpecified)
+ }
+
+ var hasAny, hasNone, hasSpecific bool
+ for _, config := range configs {
+ switch {
+ case hasDistroSpecified(config):
+ curResolvedDistros, err := s.resolveDistro(*config)
+ if err != nil {
+ return nil, fmt.Errorf("unable to resolve distro: %w", err)
+ }
+
+ if len(curResolvedDistros) == 0 {
+ return nil, ErrOSNotPresent
+ }
+ hasSpecific = true
+ for _, d := range curResolvedDistros {
+ 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.
+ hasAny = true
+ case *config == *NoOSSpecified:
+ hasNone = true
+ }
+ }
+
+ if (hasAny || hasNone) && hasSpecific {
+ return nil, fmt.Errorf("cannot mix specific distro with any or none distro specifiers")
+ }
+
+ switch {
+ case hasAny:
+ return query, nil
+ case hasNone:
+ return query.Where("operating_system_id IS NULL"), nil
+ }
+
+ query = query.Where("affected_package_handles.operating_system_id IN ?", maps.Keys(ids))
+
+ return query, nil
+}
+
+func (s *affectedPackageStore) resolveDistro(d OSSpecifier) ([]OperatingSystem, error) {
+ if d.Name == "" && d.LabelVersion == "" {
+ return nil, ErrMissingOSIdentification
+ }
+
+ // search for aliases for the given distro; we intentionally map some OSs to other OSs in terms of
+ // vulnerability (e.g. `centos` is an alias for `rhel`). If an alias is found always use that alias in
+ // searches (there will never be anything in the DB for aliased distros).
+ if err := s.applyOSAlias(&d); err != nil {
+ return nil, err
+ }
+
+ query := s.db.Model(&OperatingSystem{})
+
+ if d.Name != "" {
+ query = query.Where("name = ? collate nocase OR release_id = ? collate nocase", d.Name, d.Name)
+ }
+
+ if d.LabelVersion != "" {
+ query = query.Where("codename = ? collate nocase OR label_version = ? collate nocase", d.LabelVersion, d.LabelVersion)
+ }
+
+ return s.searchForDistroVersionVariants(query, d)
+}
+
+func (s *affectedPackageStore) applyOSAlias(d *OSSpecifier) error {
+ if d.Name == "" {
+ return nil
+ }
+
+ var aliases []OperatingSystemSpecifierOverride
+ err := s.db.Where("alias = ? collate nocase", d.Name).Find(&aliases).Error
+ if err != nil {
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
+ return fmt.Errorf("failed to resolve alias for distro %q: %w", d.Name, err)
+ }
+ return nil
+ }
+
+ var alias *OperatingSystemSpecifierOverride
+
+ for _, a := range aliases {
+ if a.Codename != "" && a.Codename != d.LabelVersion {
+ continue
+ }
+
+ if a.Version != "" && a.Version != d.version() {
+ continue
+ }
+
+ if a.VersionPattern != "" && !d.matchesVersionPattern(a.VersionPattern) {
+ continue
+ }
+
+ alias = &a
+ break
+ }
+
+ if alias == nil {
+ return nil
+ }
+
+ if alias.ReplacementName != nil {
+ d.Name = *alias.ReplacementName
+ }
+
+ if alias.Rolling {
+ d.MajorVersion = ""
+ d.MinorVersion = ""
+ }
+
+ if alias.ReplacementMajorVersion != nil {
+ d.MajorVersion = *alias.ReplacementMajorVersion
+ }
+
+ if alias.ReplacementMinorVersion != nil {
+ d.MinorVersion = *alias.ReplacementMinorVersion
+ }
+
+ if alias.ReplacementLabelVersion != nil {
+ d.LabelVersion = *alias.ReplacementLabelVersion
+ }
+
+ return nil
+}
+
+func (s *affectedPackageStore) searchForDistroVersionVariants(query *gorm.DB, d OSSpecifier) ([]OperatingSystem, error) {
+ var allOs []OperatingSystem
+
+ handleQuery := func(q *gorm.DB, desc string) ([]OperatingSystem, error) {
+ err := q.Find(&allOs).Error
+ if err == nil {
+ return allOs, nil
+ }
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, fmt.Errorf("failed to query distro by %s: %w", desc, err)
+ }
+ return nil, nil
+ }
+
+ if d.MajorVersion == "" && d.MinorVersion == "" {
+ return handleQuery(query, "name and codename only")
+ }
+
+ // search by the most specific criteria first, then fallback
+ d.MajorVersion = strings.TrimPrefix(d.MajorVersion, "0")
+ d.MinorVersion = strings.TrimPrefix(d.MinorVersion, "0")
+
+ var result []OperatingSystem
+ var err error
+ if d.MajorVersion != "" {
+ if d.MinorVersion != "" {
+ // non-empty major and minor versions
+ specificQuery := query.Session(&gorm.Session{}).Where("major_version = ? AND minor_version = ?", d.MajorVersion, d.MinorVersion)
+ result, err = handleQuery(specificQuery, "major and minor versions")
+ if err != nil || len(result) > 0 {
+ return result, err
+ }
+ }
+
+ // fallback to major version only, requiring the minor version to be blank. Note: it is important that we don't
+ // match on any record with the given major version, we must only match on records that are intentionally empty
+ // minor version. For instance, the DB may have rhel 8.1, 8.2, 8.3, 8.4, etc. We don't want to arbitrarily match
+ // on one of these or match even the latest version, as even that may yield incorrect vulnerability matching
+ // results. We are only intending to allow matches for when the vulnerability data is only specified at the major version level.
+ majorExclusiveQuery := query.Session(&gorm.Session{}).Where("major_version = ? AND minor_version = ?", d.MajorVersion, "")
+ result, err = handleQuery(majorExclusiveQuery, "exclusively major version")
+ if err != nil || len(result) > 0 {
+ return result, err
+ }
+
+ // fallback to major version for any minor version
+ majorQuery := query.Session(&gorm.Session{}).Where("major_version = ?", d.MajorVersion)
+ result, err = handleQuery(majorQuery, "major version with any minor version")
+ if err != nil || len(result) > 0 {
+ return result, err
+ }
+ }
+
+ return allOs, nil
+}
+
+func (s *affectedPackageStore) handlePreload(query *gorm.DB, config GetAffectedPackageOptions) *gorm.DB {
+ var limitArgs []interface{}
+ if config.Limit > 0 {
+ query = query.Limit(config.Limit)
+ limitArgs = append(limitArgs, func(db *gorm.DB) *gorm.DB {
+ return db.Limit(config.Limit)
+ })
+ }
+
+ if config.PreloadPackage {
+ query = query.Preload("Package", limitArgs...)
+
+ if config.PreloadPackageCPEs {
+ query = query.Preload("Package.CPEs", limitArgs...)
+ }
+ }
+
+ if config.PreloadVulnerability {
+ query = query.Preload("Vulnerability", limitArgs...).Preload("Vulnerability.Provider", limitArgs...)
+ }
+
+ if config.PreloadOS {
+ query = query.Preload("OperatingSystem", limitArgs...)
+ }
+
+ return query
+}
+
+func handleCPEOptions(query *gorm.DB, c *cpe.Attributes, allowBroad bool) *gorm.DB {
+ query = queryCPEAttributeScope(query, c.Part, "cpes.part", allowBroad)
+ query = queryCPEAttributeScope(query, c.Vendor, "cpes.vendor", allowBroad)
+ query = queryCPEAttributeScope(query, c.Product, "cpes.product", allowBroad)
+ query = queryCPEAttributeScope(query, c.Edition, "cpes.edition", allowBroad)
+ query = queryCPEAttributeScope(query, c.Language, "cpes.language", allowBroad)
+ query = queryCPEAttributeScope(query, c.SWEdition, "cpes.software_edition", allowBroad)
+ query = queryCPEAttributeScope(query, c.TargetSW, "cpes.target_software", allowBroad)
+ query = queryCPEAttributeScope(query, c.TargetHW, "cpes.target_hardware", allowBroad)
+ query = queryCPEAttributeScope(query, c.Other, "cpes.other", allowBroad)
+ return query
+}
+
+func queryCPEAttributeScope(query *gorm.DB, value string, dbColumn string, allowBroad bool) *gorm.DB {
+ if value == cpe.Any {
+ return query
+ }
+ if allowBroad {
+ // this allows for a package that specifies a CPE like
+ //
+ // 'cpe:2.3:a:cloudflare:octorpki:1.4.1:*:*:*:*:golang:*:*'
+ //
+ // to be able to positively match with a package CPE that claims to match "any" target software.
+ //
+ // 'cpe:2.3:a:cloudflare:octorpki:1.4.1:*:*:*:*:*:*:*'
+ //
+ // practically speaking, how would a vulnerability provider know that the package is vulnerable for all
+ // target software values (against the universe of packaging) -- this isn't practical.
+ return query.Where(fmt.Sprintf("%s = ? collate nocase or %s = ? collate nocase", dbColumn, dbColumn), value, cpe.Any)
+ }
+ // this is the most practical use case, where the package CPE with specified values must match the vulnerability
+ // CPE exactly (only for specified fields)
+ return query.Where(fmt.Sprintf("%s = ? collate nocase", dbColumn), value)
+}
+
+func hasDistroSpecified(d *OSSpecifier) bool {
+ if d == AnyOSSpecified {
+ return false
+ }
+
+ if *d == *NoOSSpecified {
+ return false
+ }
+ return true
+}
diff --git a/grype/db/v6/affected_package_store_test.go b/grype/db/v6/affected_package_store_test.go
new file mode 100644
index 00000000000..3f7ede29f4e
--- /dev/null
+++ b/grype/db/v6/affected_package_store_test.go
@@ -0,0 +1,1441 @@
+package v6
+
+import (
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/scylladb/go-set/strset"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/syft/syft/cpe"
+)
+
+type affectedPackageHandlePreloadConfig struct {
+ name string
+ PreloadOS bool
+ PreloadPackage bool
+ PreloadBlob bool
+ PreloadVulnerability bool
+ prepExpectations func(*testing.T, []AffectedPackageHandle) []AffectedPackageHandle
+}
+
+func defaultAffectedPackageHandlePreloadCases() []affectedPackageHandlePreloadConfig {
+ return []affectedPackageHandlePreloadConfig{
+ {
+ name: "preload-all",
+ PreloadOS: true,
+ PreloadPackage: true,
+ PreloadBlob: true,
+ PreloadVulnerability: true,
+ prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {
+ for _, a := range in {
+ if a.OperatingSystemID != nil {
+ require.NotNil(t, a.OperatingSystem)
+ }
+ require.NotNil(t, a.Package)
+ require.NotNil(t, a.BlobValue)
+ require.NotNil(t, a.Vulnerability)
+ }
+ return in
+ },
+ },
+ {
+ name: "preload-none",
+ prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {
+ var out []AffectedPackageHandle
+ for _, a := range in {
+ if a.OperatingSystem == nil && a.BlobValue == nil && a.Package == nil && a.Vulnerability == nil {
+ t.Skip("preload already matches expectation")
+ }
+ a.OperatingSystem = nil
+ a.Package = nil
+ a.BlobValue = nil
+ a.Vulnerability = nil
+ out = append(out, a)
+ }
+ return out
+ },
+ },
+ {
+ name: "preload-os-only",
+ PreloadOS: true,
+ prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {
+ var out []AffectedPackageHandle
+ for _, a := range in {
+ if a.OperatingSystemID != nil {
+ require.NotNil(t, a.OperatingSystem)
+ }
+ if a.Package == nil && a.BlobValue == nil && a.Vulnerability == nil {
+ t.Skip("preload already matches expectation")
+ }
+ a.Package = nil
+ a.BlobValue = nil
+ a.Vulnerability = nil
+ out = append(out, a)
+ }
+ return out
+ },
+ },
+ {
+ name: "preload-package-only",
+ PreloadPackage: true,
+ prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {
+ var out []AffectedPackageHandle
+ for _, a := range in {
+ require.NotNil(t, a.Package)
+ if a.OperatingSystem == nil && a.BlobValue == nil && a.Vulnerability == nil {
+ t.Skip("preload already matches expectation")
+ }
+ a.OperatingSystem = nil
+ a.BlobValue = nil
+ a.Vulnerability = nil
+ out = append(out, a)
+ }
+ return out
+ },
+ },
+ {
+ name: "preload-blob-only",
+ PreloadBlob: true,
+ prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {
+ var out []AffectedPackageHandle
+ for _, a := range in {
+ if a.OperatingSystem == nil && a.Package == nil && a.Vulnerability == nil {
+ t.Skip("preload already matches expectation")
+ }
+ a.OperatingSystem = nil
+ a.Package = nil
+ a.Vulnerability = nil
+ out = append(out, a)
+ }
+ return out
+ },
+ },
+ {
+ name: "preload-vulnerability-only",
+ PreloadVulnerability: true,
+ prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {
+ var out []AffectedPackageHandle
+ for _, a := range in {
+ if a.OperatingSystem == nil && a.Package == nil && a.BlobValue == nil {
+ t.Skip("preload already matches expectation")
+ }
+ a.OperatingSystem = nil
+ a.Package = nil
+ a.BlobValue = nil
+ out = append(out, a)
+ }
+ return out
+ },
+ },
+ }
+}
+
+func TestAffectedPackageStore_AddAffectedPackages(t *testing.T) {
+ setupAffectedPackageStore := func(t *testing.T) *affectedPackageStore {
+ db := setupTestStore(t).db
+ return newAffectedPackageStore(db, newBlobStore(db))
+ }
+
+ setupTestStoreWithPackages := func(t *testing.T) (*AffectedPackageHandle, *AffectedPackageHandle, *affectedPackageStore) {
+ pkg1 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-1234",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg1", Ecosystem: "type1"},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-1234"},
+ },
+ }
+
+ pkg2 := testDistro1AffectedPackage2Handle()
+
+ return pkg1, pkg2, setupAffectedPackageStore(t)
+ }
+
+ t.Run("no preloading", func(t *testing.T) {
+ pkg1, pkg2, s := setupTestStoreWithPackages(t)
+
+ err := s.AddAffectedPackages(pkg1, pkg2)
+ require.NoError(t, err)
+
+ var result1 AffectedPackageHandle
+ err = s.db.Where("package_id = ?", pkg1.PackageID).First(&result1).Error
+ require.NoError(t, err)
+ assert.Equal(t, pkg1.PackageID, result1.PackageID)
+ assert.Equal(t, pkg1.BlobID, result1.BlobID)
+ require.Nil(t, result1.BlobValue) // no preloading on fetch
+
+ var result2 AffectedPackageHandle
+ err = s.db.Where("package_id = ?", pkg2.PackageID).First(&result2).Error
+ require.NoError(t, err)
+ assert.Equal(t, pkg2.PackageID, result2.PackageID)
+ assert.Equal(t, pkg2.BlobID, result2.BlobID)
+ require.Nil(t, result2.BlobValue)
+ })
+
+ t.Run("preloading", func(t *testing.T) {
+ pkg1, pkg2, s := setupTestStoreWithPackages(t)
+
+ err := s.AddAffectedPackages(pkg1, pkg2)
+ require.NoError(t, err)
+
+ options := &GetAffectedPackageOptions{
+ PreloadOS: true,
+ PreloadPackage: true,
+ PreloadBlob: true,
+ }
+
+ results, err := s.GetAffectedPackages(pkgFromName(pkg1.Package.Name), options)
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+
+ result := results[0]
+ require.NotNil(t, result.Package)
+ require.NotNil(t, result.BlobValue)
+ assert.Nil(t, result.OperatingSystem) // pkg1 has no OS
+ })
+
+ t.Run("preload CPEs", func(t *testing.T) {
+ pkg1, _, s := setupTestStoreWithPackages(t)
+
+ c := Cpe{
+ Part: "a",
+ Vendor: "vendor1",
+ Product: "product1",
+ }
+ pkg1.Package.CPEs = []Cpe{c}
+
+ err := s.AddAffectedPackages(pkg1)
+ require.NoError(t, err)
+
+ options := &GetAffectedPackageOptions{
+ PreloadPackage: true,
+ PreloadPackageCPEs: true,
+ }
+
+ results, err := s.GetAffectedPackages(pkgFromName(pkg1.Package.Name), options)
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+
+ result := results[0]
+ require.NotNil(t, result.Package)
+
+ // the IDs should have been set, and there is only one, so we know the correct values
+ c.ID = 1
+
+ if d := cmp.Diff([]Cpe{c}, result.Package.CPEs); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+ })
+
+ t.Run("Package deduplication", func(t *testing.T) {
+ pkg1 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-1234",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg1", Ecosystem: "type1"},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-1234"},
+ },
+ }
+
+ pkg2 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-1234",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg1", Ecosystem: "type1"}, // same!
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-56789"},
+ },
+ }
+
+ s := setupAffectedPackageStore(t)
+ err := s.AddAffectedPackages(pkg1, pkg2)
+ require.NoError(t, err)
+
+ var pkgs []Package
+ err = s.db.Find(&pkgs).Error
+ require.NoError(t, err)
+
+ expected := []Package{
+ *pkg1.Package,
+ }
+
+ if d := cmp.Diff(expected, pkgs); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+ })
+
+ t.Run("same package with multiple CPEs", func(t *testing.T) {
+ cpe1 := Cpe{
+ Part: "a",
+ Vendor: "vendor1",
+ Product: "product1",
+ }
+
+ cpe2 := Cpe{
+ Part: "a",
+ Vendor: "vendor2",
+ Product: "product2",
+ }
+
+ pkg1 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-1234",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-1234"},
+ },
+ }
+
+ pkg2 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-56789",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1, cpe2}}, // duplicate CPE + additional CPE
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-56789"},
+ },
+ }
+
+ s := setupAffectedPackageStore(t)
+ err := s.AddAffectedPackages(pkg1, pkg2)
+ require.NoError(t, err)
+
+ var pkgs []Package
+ err = s.db.Preload("CPEs").Find(&pkgs).Error
+ require.NoError(t, err)
+
+ expPkg := *pkg1.Package
+ expPkg.ID = 1
+ cpe1.ID = 1
+ cpe2.ID = 2
+ expPkg.CPEs = []Cpe{cpe1, cpe2}
+
+ expected := []Package{
+ expPkg,
+ }
+
+ if d := cmp.Diff(expected, pkgs); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+
+ expectedCPEs := []Cpe{cpe1, cpe2}
+ var cpeResults []Cpe
+ err = s.db.Find(&cpeResults).Error
+ require.NoError(t, err)
+ if d := cmp.Diff(expectedCPEs, cpeResults); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+
+ })
+
+ t.Run("allow same CPE to belong to multiple packages", func(t *testing.T) {
+ cpe1 := Cpe{
+ Part: "a",
+ Vendor: "vendor1",
+ Product: "product1",
+ }
+
+ cpe2 := Cpe{
+ Part: "a",
+ Vendor: "vendor2",
+ Product: "product2",
+ }
+
+ pkg1 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-1234",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-1234"},
+ },
+ }
+
+ pkg2 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-56789",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg2", Ecosystem: "type1", CPEs: []Cpe{cpe1, cpe2}}, // overlapping CPEs for different packages
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-56789"},
+ },
+ }
+
+ s := setupAffectedPackageStore(t)
+ err := s.AddAffectedPackages(pkg1, pkg2)
+ require.NoError(t, err)
+
+ var pkgs []Package
+ err = s.db.Preload("CPEs").Find(&pkgs).Error
+ require.NoError(t, err)
+
+ cpe1.ID = 1
+ cpe2.ID = 2
+
+ expPkg1 := *pkg1.Package
+ expPkg1.ID = 1
+ expPkg1.CPEs = []Cpe{cpe1}
+
+ expPkg2 := *pkg2.Package
+ expPkg2.ID = 2
+ expPkg2.CPEs = []Cpe{cpe1, cpe2}
+
+ expected := []Package{
+ expPkg1,
+ expPkg2,
+ }
+
+ if d := cmp.Diff(expected, pkgs); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+
+ expectedCPEs := []Cpe{cpe1, cpe2}
+ var cpeResults []Cpe
+ err = s.db.Find(&cpeResults).Error
+ require.NoError(t, err)
+ if d := cmp.Diff(expectedCPEs, cpeResults); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+ })
+}
+
+func TestAffectedPackageStore_GetAffectedPackages_ByCPE(t *testing.T) {
+ db := setupTestStore(t).db
+ bs := newBlobStore(db)
+ s := newAffectedPackageStore(db, bs)
+
+ cpe1 := Cpe{Part: "a", Vendor: "vendor1", Product: "product1"}
+ cpe2 := Cpe{Part: "a", Vendor: "vendor2", Product: "product2"}
+ cpe3 := Cpe{Part: "a", Vendor: "vendor2", Product: "product2", TargetSoftware: "target1"}
+ pkg1 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-1234",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-1234"},
+ },
+ }
+ pkg2 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-5678",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg2", Ecosystem: "type2", CPEs: []Cpe{cpe2}},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-5678"},
+ },
+ }
+
+ pkg3 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-5678",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg3", Ecosystem: "type2", CPEs: []Cpe{cpe3}},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-5678"},
+ },
+ }
+
+ err := s.AddAffectedPackages(pkg1, pkg2, pkg3)
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ cpe cpe.Attributes
+ options *GetAffectedPackageOptions
+ expected []AffectedPackageHandle
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "full match CPE",
+ cpe: cpe.Attributes{
+ Part: "a",
+ Vendor: "vendor1",
+ Product: "product1",
+ },
+ options: &GetAffectedPackageOptions{
+ PreloadPackageCPEs: true,
+ PreloadPackage: true,
+ PreloadBlob: true,
+ PreloadVulnerability: true,
+ },
+ expected: []AffectedPackageHandle{*pkg1},
+ },
+ {
+ name: "partial match CPE",
+ cpe: cpe.Attributes{
+ Part: "a",
+ Vendor: "vendor2",
+ },
+ options: &GetAffectedPackageOptions{
+ PreloadPackageCPEs: true,
+ PreloadPackage: true,
+ PreloadBlob: true,
+ PreloadVulnerability: true,
+ },
+ expected: []AffectedPackageHandle{*pkg2, *pkg3},
+ },
+ {
+ name: "match on any TSW when specific one provided when broad matching enabled",
+ cpe: cpe.Attributes{
+ Part: "a",
+ Vendor: "vendor2",
+ TargetSW: "target1",
+ },
+ options: &GetAffectedPackageOptions{
+ PreloadPackageCPEs: true,
+ PreloadPackage: true,
+ PreloadBlob: true,
+ PreloadVulnerability: true,
+ AllowBroadCPEMatching: true,
+ },
+ expected: []AffectedPackageHandle{*pkg2, *pkg3},
+ },
+ {
+ name: "do NOT match on any TSW when specific one provided when broad matching disabled",
+ cpe: cpe.Attributes{
+ Part: "a",
+ Vendor: "vendor2",
+ TargetSW: "target1",
+ },
+ options: &GetAffectedPackageOptions{
+ PreloadPackageCPEs: true,
+ PreloadPackage: true,
+ PreloadBlob: true,
+ PreloadVulnerability: true,
+ AllowBroadCPEMatching: false,
+ },
+ expected: []AffectedPackageHandle{*pkg3},
+ },
+ {
+ name: "missing attributes",
+ cpe: cpe.Attributes{
+ Part: "a",
+ },
+ options: &GetAffectedPackageOptions{
+ PreloadPackageCPEs: true,
+ PreloadPackage: true,
+ PreloadBlob: true,
+ PreloadVulnerability: true,
+ },
+ expected: []AffectedPackageHandle{*pkg1, *pkg2, *pkg3},
+ },
+ {
+ name: "no matches",
+ cpe: cpe.Attributes{
+ Part: "a",
+ Vendor: "unknown_vendor",
+ Product: "unknown_product",
+ },
+ options: &GetAffectedPackageOptions{
+ PreloadPackageCPEs: true,
+ PreloadPackage: true,
+ PreloadBlob: true,
+ PreloadVulnerability: true,
+ },
+ expected: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.wantErr == nil {
+ tt.wantErr = require.NoError
+ }
+
+ result, err := s.GetAffectedPackages(&PackageSpecifier{CPE: &tt.cpe}, tt.options)
+ tt.wantErr(t, err)
+ if err != nil {
+ return
+ }
+ if d := cmp.Diff(tt.expected, result, cmpopts.EquateEmpty()); d != "" {
+ t.Errorf("unexpected result: %s", d)
+ }
+ })
+ }
+}
+
+func TestAffectedPackageStore_GetAffectedPackages_CaseInsensitive(t *testing.T) {
+ db := setupTestStore(t).db
+ bs := newBlobStore(db)
+ s := newAffectedPackageStore(db, bs)
+
+ cpe1 := Cpe{Part: "a", Vendor: "Vendor1", Product: "Product1"} // capitalized
+ pkg1 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-1234",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ OperatingSystem: &OperatingSystem{
+ Name: "Ubuntu", // capitalized
+ ReleaseID: "zubuntu",
+ MajorVersion: "20",
+ MinorVersion: "04", // leading 0
+ Codename: "focal",
+ },
+ Package: &Package{Name: "Pkg1", Ecosystem: "Type1", CPEs: []Cpe{cpe1}}, // capitalized
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-1234"},
+ },
+ }
+
+ pkg2 := &AffectedPackageHandle{ // this should never register as a match
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2222-2222",
+ Provider: &Provider{
+ ID: "provider2",
+ },
+ },
+ OperatingSystem: &OperatingSystem{
+ Name: "ubuntu",
+ ReleaseID: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "10",
+ },
+ Package: &Package{Name: "pkg2", Ecosystem: "type2"},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2222-2222"},
+ },
+ }
+
+ err := s.AddAffectedPackages(pkg1, pkg2)
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ pkgSpec *PackageSpecifier
+ options *GetAffectedPackageOptions
+ expected int
+ }{
+ {
+ name: "sanity check: search miss",
+ pkgSpec: pkgFromName("does not exist"),
+ expected: 0,
+ },
+ {
+ name: "get by name",
+ pkgSpec: pkgFromName("pKG1"),
+ expected: 1,
+ },
+ {
+ name: "get by CPE",
+ pkgSpec: &PackageSpecifier{
+ CPE: &cpe.Attributes{Part: "a", Vendor: "veNDor1", Product: "pRODuct1"},
+ },
+ expected: 1,
+ },
+ {
+ name: "get by ecosystem",
+ pkgSpec: &PackageSpecifier{
+ Ecosystem: "tYPE1",
+ },
+ expected: 1,
+ },
+ {
+ name: "get by OS name and version (leading 0)",
+ options: &GetAffectedPackageOptions{
+ OSs: []*OSSpecifier{{
+ Name: "uBUNtu",
+ MajorVersion: "20",
+ MinorVersion: "04",
+ }},
+ },
+ expected: 1,
+ },
+ {
+ name: "get by OS name and version",
+ options: &GetAffectedPackageOptions{
+ OSs: []*OSSpecifier{{
+ Name: "uBUNtu",
+ MajorVersion: "20",
+ MinorVersion: "4",
+ }},
+ },
+ expected: 1,
+ },
+ {
+ name: "get by OS release",
+ options: &GetAffectedPackageOptions{
+ OSs: []*OSSpecifier{{
+ Name: "zUBuntu",
+ }},
+ },
+ expected: 1,
+ },
+ {
+ name: "get by OS codename",
+ options: &GetAffectedPackageOptions{
+ OSs: []*OSSpecifier{{
+ LabelVersion: "fOCAL",
+ }},
+ },
+ expected: 1,
+ },
+ {
+ name: "get by vuln ID",
+ options: &GetAffectedPackageOptions{
+ Vulnerabilities: []VulnerabilitySpecifier{{Name: "cVe-2023-1234"}},
+ },
+ expected: 1,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := s.GetAffectedPackages(tt.pkgSpec, tt.options)
+ require.NoError(t, err)
+ require.Len(t, result, tt.expected)
+ if tt.expected > 0 {
+ assert.Equal(t, pkg1.PackageID, result[0].PackageID)
+ }
+ })
+ }
+}
+
+func TestAffectedPackageStore_GetAffectedPackages_MultipleVulnerabilitySpecs(t *testing.T) {
+ db := setupTestStore(t).db
+ bs := newBlobStore(db)
+ s := newAffectedPackageStore(db, bs)
+
+ cpe1 := Cpe{Part: "a", Vendor: "vendor1", Product: "product1"}
+ cpe2 := Cpe{Part: "a", Vendor: "vendor2", Product: "product2"}
+ pkg1 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-1234",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-1234"},
+ },
+ }
+ pkg2 := &AffectedPackageHandle{
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-5678",
+ Provider: &Provider{
+ ID: "provider1",
+ },
+ },
+ Package: &Package{Name: "pkg2", Ecosystem: "type2", CPEs: []Cpe{cpe2}},
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-5678"},
+ },
+ }
+
+ err := s.AddAffectedPackages(pkg1, pkg2)
+ require.NoError(t, err)
+
+ result, err := s.GetAffectedPackages(nil, &GetAffectedPackageOptions{
+ PreloadVulnerability: true,
+ Vulnerabilities: []VulnerabilitySpecifier{
+ {Name: "CVE-2023-1234"},
+ {Name: "CVE-2023-5678"},
+ },
+ })
+ require.NoError(t, err)
+
+ actualVulns := strset.New()
+ for _, r := range result {
+ actualVulns.Add(r.Vulnerability.Name)
+ }
+
+ expectedVulns := strset.New("CVE-2023-1234", "CVE-2023-5678")
+
+ assert.ElementsMatch(t, expectedVulns.List(), actualVulns.List())
+
+}
+
+func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) {
+ db := setupTestStore(t).db
+ bs := newBlobStore(db)
+ s := newAffectedPackageStore(db, bs)
+
+ pkg2d1 := testDistro1AffectedPackage2Handle()
+ pkg2 := testNonDistroAffectedPackage2Handle()
+ pkg2d2 := testDistro2AffectedPackage2Handle()
+ err := s.AddAffectedPackages(pkg2d1, pkg2, pkg2d2)
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ pkg *PackageSpecifier
+ options *GetAffectedPackageOptions
+ expected []AffectedPackageHandle
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "specific distro",
+ pkg: pkgFromName(pkg2d1.Package.Name),
+ options: &GetAffectedPackageOptions{
+ OSs: []*OSSpecifier{{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "04",
+ }},
+ },
+ expected: []AffectedPackageHandle{*pkg2d1},
+ },
+ {
+ name: "distro major version only",
+ pkg: pkgFromName(pkg2d1.Package.Name),
+ options: &GetAffectedPackageOptions{
+ OSs: []*OSSpecifier{{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ }},
+ },
+ expected: []AffectedPackageHandle{*pkg2d1, *pkg2d2},
+ },
+ {
+ name: "distro codename",
+ pkg: pkgFromName(pkg2d1.Package.Name),
+ options: &GetAffectedPackageOptions{
+ OSs: []*OSSpecifier{{
+ Name: "ubuntu",
+ LabelVersion: "groovy",
+ }},
+ },
+ expected: []AffectedPackageHandle{*pkg2d2},
+ },
+ {
+ name: "no distro",
+ pkg: pkgFromName(pkg2.Package.Name),
+ options: &GetAffectedPackageOptions{
+ OSs: []*OSSpecifier{NoOSSpecified},
+ },
+ expected: []AffectedPackageHandle{*pkg2},
+ },
+ {
+ name: "any distro",
+ pkg: pkgFromName(pkg2d1.Package.Name),
+ options: &GetAffectedPackageOptions{
+ OSs: []*OSSpecifier{AnyOSSpecified},
+ },
+ expected: []AffectedPackageHandle{*pkg2d1, *pkg2, *pkg2d2},
+ },
+ {
+ name: "package type",
+ pkg: &PackageSpecifier{Name: pkg2.Package.Name, Ecosystem: "type2"},
+ expected: []AffectedPackageHandle{*pkg2},
+ },
+ {
+ name: "specific CVE",
+ pkg: pkgFromName(pkg2d1.Package.Name),
+ options: &GetAffectedPackageOptions{
+ Vulnerabilities: []VulnerabilitySpecifier{{
+ Name: "CVE-2023-1234",
+ }},
+ },
+ expected: []AffectedPackageHandle{*pkg2d1},
+ },
+ {
+ name: "any CVE published after a date",
+ pkg: pkgFromName(pkg2d1.Package.Name),
+ options: &GetAffectedPackageOptions{
+ Vulnerabilities: []VulnerabilitySpecifier{{
+ PublishedAfter: func() *time.Time {
+ now := time.Date(2020, 1, 1, 1, 1, 1, 0, time.UTC)
+ return &now
+ }(),
+ }},
+ },
+ expected: []AffectedPackageHandle{*pkg2d1, *pkg2d2},
+ },
+ {
+ name: "any CVE modified after a date",
+ pkg: pkgFromName(pkg2d1.Package.Name),
+ options: &GetAffectedPackageOptions{
+ Vulnerabilities: []VulnerabilitySpecifier{{
+ ModifiedAfter: func() *time.Time {
+ now := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC).Add(time.Hour * 2)
+ return &now
+ }(),
+ }},
+ },
+ expected: []AffectedPackageHandle{*pkg2d1},
+ },
+ {
+ name: "any rejected CVE",
+ pkg: pkgFromName(pkg2d1.Package.Name),
+ options: &GetAffectedPackageOptions{
+ Vulnerabilities: []VulnerabilitySpecifier{{
+ Status: VulnerabilityRejected,
+ }},
+ },
+ expected: []AffectedPackageHandle{*pkg2d1},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.wantErr == nil {
+ tt.wantErr = require.NoError
+ }
+ for _, pc := range defaultAffectedPackageHandlePreloadCases() {
+ t.Run(pc.name, func(t *testing.T) {
+ opts := tt.options
+ if opts == nil {
+ opts = &GetAffectedPackageOptions{}
+ }
+ opts.PreloadOS = pc.PreloadOS
+ opts.PreloadPackage = pc.PreloadPackage
+ opts.PreloadBlob = pc.PreloadBlob
+ opts.PreloadVulnerability = pc.PreloadVulnerability
+ expected := tt.expected
+ if pc.prepExpectations != nil {
+ expected = pc.prepExpectations(t, expected)
+ }
+ result, err := s.GetAffectedPackages(tt.pkg, opts)
+ tt.wantErr(t, err)
+ if err != nil {
+ return
+ }
+ if d := cmp.Diff(expected, result); d != "" {
+ t.Errorf("unexpected result: %s", d)
+ }
+ })
+ }
+ })
+ }
+}
+
+func TestAffectedPackageStore_ApplyPackageAlias(t *testing.T) {
+ db := setupTestStore(t).db
+ bs := newBlobStore(db)
+ s := newAffectedPackageStore(db, bs)
+
+ tests := []struct {
+ name string
+ input *PackageSpecifier
+ expected string
+ }{
+ // positive cases
+ {name: "alias cocoapods", input: &PackageSpecifier{Ecosystem: "cocoapods"}, expected: "pod"},
+ {name: "alias pub", input: &PackageSpecifier{Ecosystem: "pub"}, expected: "dart-pub"},
+ {name: "alias otp", input: &PackageSpecifier{Ecosystem: "otp"}, expected: "erlang-otp"},
+ {name: "alias github", input: &PackageSpecifier{Ecosystem: "github"}, expected: "github-action"},
+ {name: "alias golang", input: &PackageSpecifier{Ecosystem: "golang"}, expected: "go-module"},
+ {name: "alias maven", input: &PackageSpecifier{Ecosystem: "maven"}, expected: "java-archive"},
+ {name: "alias composer", input: &PackageSpecifier{Ecosystem: "composer"}, expected: "php-composer"},
+ {name: "alias pecl", input: &PackageSpecifier{Ecosystem: "pecl"}, expected: "php-pecl"},
+ {name: "alias pypi", input: &PackageSpecifier{Ecosystem: "pypi"}, expected: "python"},
+ {name: "alias cran", input: &PackageSpecifier{Ecosystem: "cran"}, expected: "R-package"},
+ {name: "alias luarocks", input: &PackageSpecifier{Ecosystem: "luarocks"}, expected: "lua-rocks"},
+ {name: "alias cargo", input: &PackageSpecifier{Ecosystem: "cargo"}, expected: "rust-crate"},
+
+ // negative cases
+ {name: "generic type", input: &PackageSpecifier{Ecosystem: "generic/linux-kernel"}, expected: "generic/linux-kernel"},
+ {name: "empty ecosystem", input: &PackageSpecifier{Ecosystem: ""}, expected: ""},
+ {name: "matching type", input: &PackageSpecifier{Ecosystem: "python"}, expected: "python"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := s.applyPackageAlias(tt.input)
+ require.NoError(t, err)
+ assert.Equal(t, tt.expected, tt.input.Ecosystem)
+ })
+ }
+}
+
+func TestAffectedPackageStore_ResolveDistro(t *testing.T) {
+ // we always preload the OS aliases into the DB when staging for writing
+ db := setupTestStore(t).db
+ bs := newBlobStore(db)
+ s := newAffectedPackageStore(db, bs)
+
+ ubuntu2004 := &OperatingSystem{Name: "ubuntu", ReleaseID: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal"}
+ ubuntu2010 := &OperatingSystem{Name: "ubuntu", MajorVersion: "20", MinorVersion: "10", LabelVersion: "groovy"}
+ rhel8 := &OperatingSystem{Name: "rhel", ReleaseID: "rhel", MajorVersion: "8"}
+ rhel81 := &OperatingSystem{Name: "rhel", ReleaseID: "rhel", MajorVersion: "8", MinorVersion: "1"}
+ 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"}
+ 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"}
+ oracle5 := &OperatingSystem{Name: "oracle", ReleaseID: "ol", MajorVersion: "5"}
+ oracle6 := &OperatingSystem{Name: "oracle", ReleaseID: "ol", MajorVersion: "6"}
+ amazon2 := &OperatingSystem{Name: "amazon", ReleaseID: "amzn", MajorVersion: "2"}
+ rocky8 := &OperatingSystem{Name: "rocky", ReleaseID: "rocky", MajorVersion: "8"} // should not be matched
+ alma8 := &OperatingSystem{Name: "almalinux", ReleaseID: "almalinux", MajorVersion: "8"} // should not be matched
+
+ operatingSystems := []*OperatingSystem{
+ ubuntu2004,
+ ubuntu2010,
+ rhel8,
+ rhel81,
+ debian10,
+ alpine318,
+ alpineEdge,
+ debianUnstable,
+ debian7,
+ wolfi,
+ arch,
+ oracle5,
+ oracle6,
+ amazon2,
+ rocky8,
+ alma8,
+ }
+ require.NoError(t, db.Create(&operatingSystems).Error)
+
+ tests := []struct {
+ name string
+ distro OSSpecifier
+ expected []OperatingSystem
+ expectErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "specific distro with major and minor version",
+ distro: OSSpecifier{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "04",
+ },
+ expected: []OperatingSystem{*ubuntu2004},
+ },
+ {
+ name: "specific distro with major and minor version (missing left padding)",
+ distro: OSSpecifier{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "4",
+ },
+ expected: []OperatingSystem{*ubuntu2004},
+ },
+ {
+ name: "alias resolution with major version",
+ distro: OSSpecifier{
+ Name: "centos",
+ MajorVersion: "8",
+ },
+ expected: []OperatingSystem{*rhel8},
+ },
+ {
+ name: "alias resolution with major and minor version",
+ distro: OSSpecifier{
+ Name: "centos",
+ MajorVersion: "8",
+ MinorVersion: "1",
+ },
+ expected: []OperatingSystem{*rhel81},
+ },
+ {
+ name: "distro with major version only",
+ distro: OSSpecifier{
+ Name: "debian",
+ MajorVersion: "10",
+ },
+ expected: []OperatingSystem{*debian10},
+ },
+ {
+ name: "codename resolution",
+ distro: OSSpecifier{
+ Name: "ubuntu",
+ LabelVersion: "focal",
+ },
+ expected: []OperatingSystem{*ubuntu2004},
+ },
+ {
+ name: "codename and version info",
+ distro: OSSpecifier{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "04",
+ LabelVersion: "focal",
+ },
+ expected: []OperatingSystem{*ubuntu2004},
+ },
+ {
+ name: "conflicting codename and version info",
+ distro: OSSpecifier{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "04",
+ LabelVersion: "fake",
+ },
+ },
+ {
+ name: "alpine edge version",
+ distro: OSSpecifier{
+ Name: "alpine",
+ MajorVersion: "3",
+ MinorVersion: "21",
+ LabelVersion: "3.21.0_alpha20240807",
+ },
+ expected: []OperatingSystem{*alpineEdge},
+ },
+ {
+ name: "arch rolling variant",
+ distro: OSSpecifier{
+ Name: "arch",
+ },
+ expected: []OperatingSystem{*arch},
+ },
+ {
+ name: "wolfi rolling variant",
+ distro: OSSpecifier{
+ Name: "wolfi",
+ MajorVersion: "20221018",
+ },
+ expected: []OperatingSystem{*wolfi},
+ },
+ {
+ name: "debian by codename for rolling alias",
+ distro: OSSpecifier{
+ Name: "debian",
+ MajorVersion: "13",
+ LabelVersion: "trixie",
+ },
+ expected: []OperatingSystem{*debianUnstable},
+ },
+ {
+ name: "debian by codename",
+ distro: OSSpecifier{
+ Name: "debian",
+ LabelVersion: "wheezy",
+ },
+ expected: []OperatingSystem{*debian7},
+ },
+ {
+ name: "debian by major version",
+ distro: OSSpecifier{
+ Name: "debian",
+ MajorVersion: "7",
+ },
+ expected: []OperatingSystem{*debian7},
+ },
+ {
+ name: "debian by major.minor version",
+ distro: OSSpecifier{
+ Name: "debian",
+ MajorVersion: "7",
+ MinorVersion: "2",
+ },
+ expected: []OperatingSystem{*debian7},
+ },
+ {
+ name: "alpine with major and minor version",
+ distro: OSSpecifier{
+ Name: "alpine",
+ MajorVersion: "3",
+ MinorVersion: "18",
+ },
+ expected: []OperatingSystem{*alpine318},
+ },
+ {
+ name: "lookup by release ID (not name)",
+ distro: OSSpecifier{
+ Name: "ol",
+ MajorVersion: "5",
+ },
+ expected: []OperatingSystem{*oracle5},
+ },
+ {
+ name: "lookup by non-standard name (oraclelinux)",
+ distro: OSSpecifier{
+ Name: "oraclelinux", // based on the grype distro names
+ MajorVersion: "5",
+ },
+ expected: []OperatingSystem{*oracle5},
+ },
+ {
+ name: "lookup by non-standard name (amazonlinux)",
+ distro: OSSpecifier{
+ Name: "amazonlinux", // based on the grype distro names
+ MajorVersion: "2",
+ },
+ expected: []OperatingSystem{*amazon2},
+ },
+ {
+ name: "lookup by non-standard name (oracle)",
+ distro: OSSpecifier{
+ Name: "oracle",
+ MajorVersion: "5",
+ },
+ expected: []OperatingSystem{*oracle5},
+ },
+ {
+ name: "lookup by non-standard name (amazon)",
+ distro: OSSpecifier{
+ Name: "amazon",
+ MajorVersion: "2",
+ },
+ expected: []OperatingSystem{*amazon2},
+ },
+ {
+ name: "lookup by non-standard name (rocky)",
+ distro: OSSpecifier{
+ Name: "rocky",
+ MajorVersion: "8",
+ },
+ expected: []OperatingSystem{*rhel8},
+ },
+ {
+ name: "lookup by non-standard name (rockylinux)",
+ distro: OSSpecifier{
+ Name: "rockylinux",
+ MajorVersion: "8",
+ },
+ expected: []OperatingSystem{*rhel8},
+ },
+ {
+ name: "lookup by non-standard name (alma)",
+ distro: OSSpecifier{
+ Name: "alma",
+ MajorVersion: "8",
+ },
+ expected: []OperatingSystem{*rhel8},
+ },
+ {
+ name: "lookup by non-standard name (almalinux)",
+ distro: OSSpecifier{
+ Name: "almalinux",
+ MajorVersion: "8",
+ },
+ expected: []OperatingSystem{*rhel8},
+ },
+ {
+ name: "missing distro name",
+ distro: OSSpecifier{
+ MajorVersion: "8",
+ },
+ expectErr: expectErrIs(t, ErrMissingOSIdentification),
+ },
+ {
+ name: "nonexistent distro",
+ distro: OSSpecifier{
+ Name: "madeup",
+ MajorVersion: "99",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectErr == nil {
+ tt.expectErr = require.NoError
+ }
+ result, err := s.resolveDistro(tt.distro)
+ tt.expectErr(t, err)
+ if err != nil {
+ return
+ }
+
+ if diff := cmp.Diff(tt.expected, result, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
+func TestDistroSpecifier_String(t *testing.T) {
+ tests := []struct {
+ name string
+ distro *OSSpecifier
+ expected string
+ }{
+ {
+ name: "nil distro",
+ distro: AnyOSSpecified,
+ expected: "any",
+ },
+ {
+ name: "no distro specified",
+ distro: NoOSSpecified,
+ expected: "none",
+ },
+ {
+ name: "only name specified",
+ distro: &OSSpecifier{
+ Name: "ubuntu",
+ },
+ expected: "ubuntu",
+ },
+ {
+ name: "name and major version specified",
+ distro: &OSSpecifier{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ },
+ expected: "ubuntu@20",
+ },
+ {
+ name: "name, major, and minor version specified",
+ distro: &OSSpecifier{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "04",
+ },
+ expected: "ubuntu@20.04",
+ },
+ {
+ name: "name, major version, and codename specified",
+ distro: &OSSpecifier{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ LabelVersion: "focal",
+ },
+ expected: "ubuntu@20 (focal)",
+ },
+ {
+ name: "name and codename specified",
+ distro: &OSSpecifier{
+ Name: "ubuntu",
+ LabelVersion: "focal",
+ },
+ expected: "ubuntu@focal",
+ },
+ {
+ name: "name, major version, minor version, and codename specified",
+ distro: &OSSpecifier{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "04",
+ LabelVersion: "focal",
+ },
+ expected: "ubuntu@20.04",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := tt.distro.String()
+ require.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+func testDistro1AffectedPackage2Handle() *AffectedPackageHandle {
+ now := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC)
+ later := now.Add(time.Hour * 200)
+ return &AffectedPackageHandle{
+ Package: &Package{
+ Name: "pkg2",
+ Ecosystem: "type2d",
+ },
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-1234",
+ Status: VulnerabilityRejected,
+ PublishedDate: &now,
+ ModifiedDate: &later,
+ Provider: &Provider{
+ ID: "ubuntu",
+ },
+ },
+ OperatingSystem: &OperatingSystem{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "04",
+ LabelVersion: "focal",
+ },
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-1234"},
+ },
+ }
+}
+
+func testDistro2AffectedPackage2Handle() *AffectedPackageHandle {
+ now := time.Date(2020, 1, 1, 3, 4, 5, 0, time.UTC)
+ later := now.Add(time.Hour * 200)
+ return &AffectedPackageHandle{
+ Package: &Package{
+ Name: "pkg2",
+ Ecosystem: "type2d",
+ },
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-4567",
+ PublishedDate: &now,
+ ModifiedDate: &later,
+ Provider: &Provider{
+ ID: "ubuntu",
+ },
+ },
+ OperatingSystem: &OperatingSystem{
+ Name: "ubuntu",
+ MajorVersion: "20",
+ MinorVersion: "10",
+ LabelVersion: "groovy",
+ },
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-4567"},
+ },
+ }
+}
+
+func testNonDistroAffectedPackage2Handle() *AffectedPackageHandle {
+ now := time.Date(2005, 1, 1, 3, 4, 5, 0, time.UTC)
+ later := now.Add(time.Hour * 200)
+ return &AffectedPackageHandle{
+ Package: &Package{
+ Name: "pkg2",
+ Ecosystem: "type2",
+ },
+ Vulnerability: &VulnerabilityHandle{
+ Name: "CVE-2023-4567",
+ PublishedDate: &now,
+ ModifiedDate: &later,
+ Provider: &Provider{
+ ID: "wolfi",
+ },
+ },
+ BlobValue: &AffectedPackageBlob{
+ CVEs: []string{"CVE-2023-4567"},
+ },
+ }
+}
+
+func expectErrIs(t *testing.T, expected error) require.ErrorAssertionFunc {
+ t.Helper()
+ return func(t require.TestingT, err error, msgAndArgs ...interface{}) {
+ require.Error(t, err, msgAndArgs...)
+ assert.ErrorIs(t, err, expected)
+ }
+}
+
+func pkgFromName(name string) *PackageSpecifier {
+ return &PackageSpecifier{Name: name}
+}
diff --git a/grype/db/v6/blob_store.go b/grype/db/v6/blob_store.go
new file mode 100644
index 00000000000..f124a0f18b5
--- /dev/null
+++ b/grype/db/v6/blob_store.go
@@ -0,0 +1,135 @@
+package v6
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ "gorm.io/gorm"
+
+ "github.com/anchore/grype/internal/log"
+)
+
+type blobable interface {
+ getBlobID() ID
+ getBlobValue() any
+ setBlobID(ID)
+ setBlob([]byte) error
+}
+
+type blobStore struct {
+ db *gorm.DB
+ idsByDigest map[string]ID
+}
+
+func newBlobStore(db *gorm.DB) *blobStore {
+ return &blobStore{
+ db: db,
+ idsByDigest: make(map[string]ID),
+ }
+}
+
+func (s *blobStore) addBlobable(bs ...blobable) error {
+ for i := range bs {
+ b := bs[i]
+ v := b.getBlobValue()
+ if v == nil {
+ continue
+ }
+ bl := newBlob(v)
+
+ if err := s.addBlobs(bl); err != nil {
+ return err
+ }
+
+ b.setBlobID(bl.ID)
+ }
+ return nil
+}
+
+func (s *blobStore) addBlobs(blobs ...*Blob) error {
+ for i := range blobs {
+ v := blobs[i]
+ digest := v.computeDigest()
+
+ if id, ok := s.idsByDigest[digest]; ok && id != 0 {
+ v.ID = id
+ continue
+ }
+
+ if err := s.db.Create(v).Error; err != nil {
+ return fmt.Errorf("failed to create blob: %w", err)
+ }
+
+ if v.ID != 0 {
+ s.idsByDigest[digest] = v.ID
+ }
+ }
+ return nil
+}
+
+func (s *blobStore) getBlobValues(ids ...ID) ([]Blob, error) {
+ if len(ids) == 0 {
+ return nil, nil
+ }
+ var blobs []Blob
+ if err := s.db.Where("id IN ?", ids).Find(&blobs).Error; err != nil {
+ return nil, fmt.Errorf("failed to get blob values: %w", err)
+ }
+ return blobs, nil
+}
+
+func (s *blobStore) attachBlobValue(bs ...blobable) error {
+ start := time.Now()
+ defer func() {
+ log.WithFields("duration", time.Since(start), "count", len(bs)).Trace("attached blob values")
+ }()
+ var ids []ID
+ var setterByID = make(map[ID][]blobable)
+ for i := range bs {
+ b := bs[i]
+
+ id := b.getBlobID()
+
+ // skip fetching this blob if there is no blobID, or if we already have this blob
+ if id == 0 || b.getBlobValue() != nil {
+ continue
+ }
+
+ ids = append(ids, id)
+ setterByID[id] = append(setterByID[id], b)
+ }
+
+ vs, err := s.getBlobValues(ids...)
+ if err != nil {
+ return fmt.Errorf("failed to get blob value: %w", err)
+ }
+
+ for _, b := range vs {
+ if b.Value == "" {
+ continue
+ }
+ for _, setter := range setterByID[b.ID] {
+ if err := setter.setBlob([]byte(b.Value)); err != nil {
+ return fmt.Errorf("failed to set blob value: %w", err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func newBlob(obj any) *Blob {
+ sb := strings.Builder{}
+ enc := json.NewEncoder(&sb)
+ enc.SetEscapeHTML(false)
+
+ if err := enc.Encode(obj); err != nil {
+ panic("could not marshal object to json")
+ }
+
+ return &Blob{
+ Value: sb.String(),
+ }
+}
diff --git a/grype/db/v6/blob_store_test.go b/grype/db/v6/blob_store_test.go
new file mode 100644
index 00000000000..86a8d5e6bf9
--- /dev/null
+++ b/grype/db/v6/blob_store_test.go
@@ -0,0 +1,40 @@
+package v6
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestBlobWriter_AddBlobs(t *testing.T) {
+ db := setupTestStore(t).db
+ writer := newBlobStore(db)
+
+ obj1 := map[string]string{"key": "value1"}
+ obj2 := map[string]string{"key": "value2"}
+
+ blob1 := newBlob(obj1)
+ blob2 := newBlob(obj2)
+ blob3 := newBlob(obj1) // same as blob1
+
+ err := writer.addBlobs(blob1, blob2, blob3)
+ require.NoError(t, err)
+
+ require.NotZero(t, blob1.ID)
+ require.Equal(t, blob1.ID, blob3.ID) // blob3 should have the same ID as blob1 (natural deduplication)
+
+ var result1 Blob
+ require.NoError(t, db.Where("id = ?", blob1.ID).First(&result1).Error)
+ assert.Equal(t, blob1.Value, result1.Value)
+
+ var result2 Blob
+ require.NoError(t, db.Where("id = ?", blob2.ID).First(&result2).Error)
+ assert.Equal(t, blob2.Value, result2.Value)
+}
+
+func TestBlob_computeDigest(t *testing.T) {
+ assert.Equal(t, "xxh64:0e6882304e9adbd5", Blob{Value: "test content"}.computeDigest())
+
+ assert.Equal(t, "xxh64:ea0c19ae9fbd93b3", Blob{Value: "different content"}.computeDigest())
+}
diff --git a/grype/db/v6/blobs.go b/grype/db/v6/blobs.go
new file mode 100644
index 00000000000..92f865f0517
--- /dev/null
+++ b/grype/db/v6/blobs.go
@@ -0,0 +1,215 @@
+package v6
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+)
+
+// VulnerabilityBlob represents the core advisory record for a single known vulnerability from a specific provider.
+type VulnerabilityBlob struct {
+ // ID is the lowercase unique string identifier for the vulnerability relative to the provider
+ ID string `json:"id"`
+
+ // Assigners is a list of names, email, or organizations who submitted the vulnerability
+ Assigners []string `json:"assigner,omitempty"`
+
+ // Description of the vulnerability as provided by the source
+ Description string `json:"description,omitempty"`
+
+ // References are URLs to external resources that provide more information about the vulnerability
+ References []Reference `json:"refs,omitempty"`
+
+ // Aliases is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases.
+ Aliases []string `json:"aliases,omitempty"`
+
+ // Severities is a list of severity indications (quantitative or qualitative) for the vulnerability
+ Severities []Severity `json:"severities,omitempty"`
+}
+
+func (v VulnerabilityBlob) String() string {
+ return v.ID
+}
+
+// Reference represents a single external URL and string tags to use for organizational purposes
+type Reference struct {
+ // URL is the external resource
+ URL string `json:"url"`
+
+ // Tags is a free-form organizational field to convey additional information about the reference
+ Tags []string `json:"tags,omitempty"`
+}
+
+// Severity represents a single string severity record for a vulnerability record
+type Severity struct {
+ // Scheme describes the quantitative method used to determine the Score, such as "CVSS_V3". Alternatively this makes
+ // claim that Value is qualitative, for example "HML" (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)
+ Scheme SeverityScheme `json:"scheme"`
+
+ // Value 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" )
+ Value any `json:"value"` // one of CVSSSeverity, HMLSeverity, CHMLNSeverity
+
+ // Source is the name of the source of the severity score (e.g. "nvd@nist.gov" or "security-advisories@github.com")
+ Source string `json:"source,omitempty"`
+
+ // Rank is a free-form organizational field to convey priority over other severities
+ Rank int `json:"rank"`
+}
+
+type severityAlias Severity
+
+type severityUnmarshalProxy struct {
+ *severityAlias
+ Value json.RawMessage `json:"value"`
+}
+
+// UnmarshalJSON custom unmarshaller for Severity struct
+func (s *Severity) UnmarshalJSON(data []byte) error {
+ aux := &severityUnmarshalProxy{
+ severityAlias: (*severityAlias)(s),
+ }
+
+ if err := json.Unmarshal(data, aux); err != nil {
+ return err
+ }
+
+ var cvss CVSSSeverity
+ if err := json.Unmarshal(aux.Value, &cvss); err == nil && cvss.Vector != "" {
+ s.Value = cvss
+ return nil
+ }
+
+ var strSeverity string
+ if err := json.Unmarshal(aux.Value, &strSeverity); err == nil {
+ s.Value = strSeverity
+ return nil
+ }
+
+ return fmt.Errorf("could not unmarshal severity value to known type: %s", aux.Value)
+}
+
+// CVSSSeverity represents a single Common Vulnerability Scoring System entry
+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"`
+}
+
+func (c CVSSSeverity) String() string {
+ vector := c.Vector
+ if !strings.HasPrefix(strings.ToLower(c.Vector), "cvss:") && c.Version != "" {
+ vector = fmt.Sprintf("CVSS:%s/%s", c.Version, c.Vector)
+ }
+ return vector
+}
+
+// AffectedPackageBlob represents a package affected by a vulnerability.
+type AffectedPackageBlob struct {
+ // CVEs is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.
+ CVEs []string `json:"cves,omitempty"`
+
+ // Qualifiers are package attributes that confirm the package is affected by the vulnerability.
+ Qualifiers *AffectedPackageQualifiers `json:"qualifiers,omitempty"`
+
+ // Ranges specifies the affected version ranges and fixes if available.
+ Ranges []AffectedRange `json:"ranges,omitempty"`
+}
+
+func (a AffectedPackageBlob) String() string {
+ var fields []string
+
+ if len(a.Ranges) > 0 {
+ var ranges []string
+ for _, r := range a.Ranges {
+ ranges = append(ranges, r.String())
+ }
+ fields = append(fields, fmt.Sprintf("ranges=%s", strings.Join(ranges, ", ")))
+ }
+
+ if len(a.CVEs) > 0 {
+ fields = append(fields, fmt.Sprintf("cves=%s", strings.Join(a.CVEs, ", ")))
+ }
+
+ return strings.Join(fields, ", ")
+}
+
+// AffectedPackageQualifiers contains package attributes that confirm the package is affected by the vulnerability.
+type AffectedPackageQualifiers struct {
+ // RpmModularity indicates if the package follows RPM modularity for versioning.
+ RpmModularity *string `json:"rpm_modularity,omitempty"`
+
+ // PlatformCPEs lists Common Platform Enumeration (CPE) identifiers for affected platforms.
+ PlatformCPEs []string `json:"platform_cpes,omitempty"`
+}
+
+// AffectedRange defines a specific range of versions affected by a vulnerability.
+type AffectedRange struct {
+ // Version defines the version constraints for affected software.
+ Version AffectedVersion `json:"version,omitempty"`
+
+ // Fix provides details on the fix version and its state if available.
+ Fix *Fix `json:"fix,omitempty"`
+}
+
+func (a AffectedRange) String() string {
+ return fmt.Sprintf("%s (%s)", a.Version, a.Fix)
+}
+
+// Fix conveys availability of a fix for a vulnerability.
+type Fix struct {
+ // Version is the version number of the fix.
+ Version string `json:"version,omitempty"`
+
+ // State represents the status of the fix (e.g., "fixed", "unaffected").
+ State FixStatus `json:"state,omitempty"`
+
+ // Detail provides additional fix information, such as commit details.
+ Detail *FixDetail `json:"detail,omitempty"`
+}
+
+func (f Fix) String() string {
+ switch f.State {
+ case FixedStatus:
+ return fmt.Sprintf("fixed in %s", f.Version)
+ case NotAffectedFixStatus:
+ return fmt.Sprintf("%s is not affected", f.Version)
+ }
+ return string(f.State)
+}
+
+// FixDetail is additional information about a fix, such as commit details and patch URLs.
+type FixDetail struct {
+ // GitCommit is the identifier for the Git commit associated with the fix.
+ GitCommit string `json:"git_commit,omitempty"`
+
+ // Timestamp is the date and time when the fix was committed.
+ Timestamp *time.Time `json:"timestamp,omitempty"`
+
+ // References contains URLs or identifiers for additional resources on the fix.
+ References []Reference `json:"references,omitempty"`
+}
+
+// AffectedVersion defines the versioning format and constraints.
+type AffectedVersion struct {
+ // Type specifies the versioning system used (e.g., "semver", "rpm").
+ Type string `json:"type,omitempty"`
+
+ // Constraint defines the version range constraint for affected versions.
+ Constraint string `json:"constraint,omitempty"`
+}
+
+type KnownExploitedVulnerabilityBlob struct {
+ Cve string `json:"cve"`
+ VendorProject string `json:"vendor_project,omitempty"`
+ Product string `json:"product,omitempty"`
+ DateAdded *time.Time `json:"date_added,omitempty"`
+ RequiredAction string `json:"required_action,omitempty"`
+ DueDate *time.Time `json:"due_date,omitempty"`
+ KnownRansomwareCampaignUse string `json:"known_ransomware_campaign_use,omitempty"`
+ Notes string `json:"notes,omitempty"`
+ URLs []string `json:"urls,omitempty"`
+ CWEs []string `json:"cwes,omitempty"`
+}
diff --git a/grype/db/v6/cache.go b/grype/db/v6/cache.go
new file mode 100644
index 00000000000..3ba0eb57b08
--- /dev/null
+++ b/grype/db/v6/cache.go
@@ -0,0 +1,113 @@
+package v6
+
+import (
+ "context"
+ "sync"
+)
+
+const (
+ cpesTableCacheKey = "cpes"
+ packagesTableCacheKey = "packages"
+ operatingSystemsTableCacheKey = "operating_systems"
+ vulnerabilitiesTableCacheKey = "vulnerabilities"
+)
+
+const cacheKey = contextKey("multiModelCache")
+
+type contextKey string
+
+type cachable interface {
+ cacheKey() string
+ tableName() string
+}
+
+type cacheIDManager interface {
+ rowID() ID
+ setRowID(ID)
+}
+
+type cacheStringIDManager interface {
+ rowID() string
+ setRowID(string)
+}
+
+func withCacheContext(ctx context.Context, c *cache) context.Context {
+ return context.WithValue(ctx, cacheKey, c)
+}
+
+func cacheFromContext(ctx context.Context) (*cache, bool) {
+ c, ok := ctx.Value(cacheKey).(*cache)
+ return c, ok
+}
+
+type cache struct {
+ mu sync.RWMutex
+ idKeys map[string]map[string]ID
+ strKeys map[string]map[string]string
+}
+
+func newCache() *cache {
+ return &cache{
+ idKeys: make(map[string]map[string]ID),
+ strKeys: make(map[string]map[string]string),
+ }
+}
+
+func (c *cache) getID(ca cachable) (ID, bool) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ if tableCache, exists := c.idKeys[ca.tableName()]; exists {
+ id, found := tableCache[ca.cacheKey()]
+ return id, found
+ }
+ return 0, false
+}
+
+func (c *cache) getString(ca cachable) (string, bool) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ if tableCache, exists := c.strKeys[ca.tableName()]; exists {
+ id, found := tableCache[ca.cacheKey()]
+ return id, found
+ }
+ return "", false
+}
+
+func (c *cache) set(ca cachable) {
+ switch cam := ca.(type) {
+ case cacheIDManager:
+ c.setIDEntry(cam.rowID(), ca)
+ case cacheStringIDManager:
+ c.setStringEntry(cam.rowID(), ca)
+ default:
+ panic("unsupported cacheable type")
+ }
+}
+
+func (c *cache) setStringEntry(id string, ca cachable) {
+ table := ca.tableName()
+ key := ca.cacheKey()
+
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if _, exists := c.strKeys[table]; !exists {
+ c.strKeys[table] = make(map[string]string)
+ }
+
+ c.strKeys[table][key] = id
+}
+
+func (c *cache) setIDEntry(id ID, ca cachable) {
+ table := ca.tableName()
+ key := ca.cacheKey()
+
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if _, exists := c.idKeys[table]; !exists {
+ c.idKeys[table] = make(map[string]ID)
+ }
+
+ c.idKeys[table][key] = id
+}
diff --git a/grype/db/v6/cache_test.go b/grype/db/v6/cache_test.go
new file mode 100644
index 00000000000..b2caf81aaeb
--- /dev/null
+++ b/grype/db/v6/cache_test.go
@@ -0,0 +1,150 @@
+package v6
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+type mockCachableID struct {
+ key string
+ table string
+ id ID
+}
+
+func (m *mockCachableID) cacheKey() string { return m.key }
+func (m *mockCachableID) tableName() string { return m.table }
+func (m *mockCachableID) rowID() ID { return m.id }
+func (m *mockCachableID) setRowID(id ID) { m.id = id }
+
+type mockCachableString struct {
+ key string
+ table string
+ id string
+}
+
+func (m *mockCachableString) cacheKey() string { return m.key }
+func (m *mockCachableString) tableName() string { return m.table }
+func (m *mockCachableString) rowID() string { return m.id }
+func (m *mockCachableString) setRowID(id string) { m.id = id }
+
+func newTestCachableString(key, table, id string) *mockCachableString {
+ return &mockCachableString{key: key, table: table, id: id}
+}
+
+func newTestCachableID(key, table string, id ID) *mockCachableID {
+ return &mockCachableID{key: key, table: table, id: id}
+}
+
+func TestCache_GetString_Found(t *testing.T) {
+ c := newCache()
+ item := newTestCachableString("test-key", "test-table", "test-id")
+
+ c.setStringEntry("test-id", item)
+
+ str, found := c.getString(item)
+ require.True(t, found)
+ require.Equal(t, "test-id", str)
+}
+
+func TestCache_GetString_NotFound(t *testing.T) {
+ c := newCache()
+ item := newTestCachableString("missing-key", "test-table", "")
+
+ str, found := c.getString(item)
+ require.False(t, found)
+ require.Empty(t, str)
+}
+
+func TestCache_Set_ID(t *testing.T) {
+ c := newCache()
+ item := newTestCachableID("test-key", "test-table", 123)
+
+ c.set(item)
+
+ id, found := c.getID(item)
+ require.True(t, found)
+ require.Equal(t, ID(123), id)
+}
+
+func TestCache_Set_String(t *testing.T) {
+ c := newCache()
+ item := newTestCachableString("test-key", "test-table", "test-id")
+
+ c.set(item)
+
+ str, found := c.getString(item)
+ require.True(t, found)
+ require.Equal(t, "test-id", str)
+}
+
+func TestCache_Set_Panic(t *testing.T) {
+ c := newCache()
+ invalidItem := struct{ cachable }{}
+
+ require.PanicsWithValue(t, "unsupported cacheable type", func() {
+ c.set(invalidItem)
+ })
+}
+
+func TestCache_SetStringEntry_New(t *testing.T) {
+ c := newCache()
+ item := newTestCachableString("test-key", "test-table", "")
+
+ c.setStringEntry("new-id", item)
+
+ str, found := c.getString(item)
+ require.True(t, found)
+ require.Equal(t, "new-id", str)
+}
+
+func TestCache_SetStringEntry_Update(t *testing.T) {
+ c := newCache()
+ item := newTestCachableString("test-key", "test-table", "old-id")
+
+ c.setStringEntry("old-id", item)
+ c.setStringEntry("new-id", item)
+
+ str, found := c.getString(item)
+ require.True(t, found)
+ require.Equal(t, "new-id", str)
+}
+
+func TestCache_SetIDEntry_New(t *testing.T) {
+ c := newCache()
+ item := newTestCachableID("test-key", "test-table", 0)
+
+ c.setIDEntry(123, item)
+
+ id, found := c.getID(item)
+ require.True(t, found)
+ require.Equal(t, ID(123), id)
+}
+
+func TestCache_SetIDEntry_Update(t *testing.T) {
+ c := newCache()
+ item := newTestCachableID("test-key", "test-table", 123)
+
+ c.setIDEntry(123, item)
+ c.setIDEntry(456, item)
+
+ id, found := c.getID(item)
+ require.True(t, found)
+ require.Equal(t, ID(456), id)
+}
+
+func TestWithCacheContext(t *testing.T) {
+ c := newCache()
+ ctx := withCacheContext(context.Background(), c)
+
+ cache, ok := cacheFromContext(ctx)
+ require.True(t, ok)
+ require.Equal(t, c, cache)
+}
+
+func TestCacheFromContext_NotFound(t *testing.T) {
+ cache, ok := cacheFromContext(context.Background())
+ require.False(t, ok)
+ require.Nil(t, cache)
+}
diff --git a/grype/db/v6/data.go b/grype/db/v6/data.go
new file mode 100644
index 00000000000..df4fbe34e2b
--- /dev/null
+++ b/grype/db/v6/data.go
@@ -0,0 +1,105 @@
+package v6
+
+import (
+ "strings"
+
+ "github.com/anchore/syft/syft/pkg"
+)
+
+// TODO: in a future iteration these should be raised up more explicitly by the vunnel providers
+func KnownOperatingSystemSpecifierOverrides() []OperatingSystemSpecifierOverride {
+ strRef := func(s string) *string {
+ return &s
+ }
+ return []OperatingSystemSpecifierOverride{
+ {Alias: "centos", ReplacementName: strRef("rhel")},
+ {Alias: "rocky", ReplacementName: strRef("rhel")},
+ {Alias: "rockylinux", ReplacementName: strRef("rhel")}, // non-standard, but common (dockerhub uses "rockylinux")
+ {Alias: "alma", ReplacementName: strRef("rhel")},
+ {Alias: "almalinux", ReplacementName: strRef("rhel")}, // non-standard, but common (dockerhub uses "almalinux")
+ {Alias: "gentoo", ReplacementName: strRef("rhel")},
+ {Alias: "alpine", VersionPattern: ".*_alpha.*", ReplacementLabelVersion: strRef("edge"), Rolling: true},
+ {Alias: "wolfi", Rolling: true},
+ {Alias: "chainguard", Rolling: true},
+ {Alias: "arch", Rolling: true},
+ {Alias: "archlinux", ReplacementName: strRef("arch"), Rolling: true}, // non-standard, but common (dockerhub uses "archlinux")
+ {Alias: "oracle", ReplacementName: strRef("ol")}, // non-standard, but common
+ {Alias: "oraclelinux", ReplacementName: strRef("ol")}, // non-standard, but common (dockerhub uses "oraclelinux")
+ {Alias: "amazon", ReplacementName: strRef("amzn")}, // non-standard, but common
+ {Alias: "amazonlinux", ReplacementName: strRef("amzn")}, // non-standard, but common (dockerhub uses "amazonlinux")
+ // TODO: trixie is a placeholder for now, but should be updated to sid when the time comes
+ // this needs to be automated, but isn't clear how to do so since you'll see things like this:
+ //
+ // ❯ docker run --rm debian:sid cat /etc/os-release | grep VERSION_CODENAME
+ // VERSION_CODENAME=trixie
+ // ❯ docker run --rm debian:testing cat /etc/os-release | grep VERSION_CODENAME
+ // VERSION_CODENAME=trixie
+ //
+ // ❯ curl -s http://deb.debian.org/debian/dists/testing/Release | grep '^Codename:'
+ // Codename: trixie
+ // ❯ curl -s http://deb.debian.org/debian/dists/sid/Release | grep '^Codename:'
+ // Codename: sid
+ //
+ // 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, ReplacementLabelVersion: strRef("unstable")}, // is currently sid, which is considered rolling
+ }
+}
+
+func KnownPackageSpecifierOverrides() []PackageSpecifierOverride {
+ // when matching packages, grype will always attempt to do so based off of the package type which means
+ // that any request must be in terms of the package type (relative to syft).
+
+ ret := []PackageSpecifierOverride{
+ // map all known language ecosystems to their respective syft package types
+ {Ecosystem: pkg.Dart.String(), ReplacementEcosystem: ptr(string(pkg.DartPubPkg))},
+ {Ecosystem: pkg.Dotnet.String(), ReplacementEcosystem: ptr(string(pkg.DotnetPkg))},
+ {Ecosystem: pkg.Elixir.String(), ReplacementEcosystem: ptr(string(pkg.HexPkg))},
+ {Ecosystem: pkg.Erlang.String(), ReplacementEcosystem: ptr(string(pkg.ErlangOTPPkg))},
+ {Ecosystem: pkg.Go.String(), ReplacementEcosystem: ptr(string(pkg.GoModulePkg))},
+ {Ecosystem: pkg.Haskell.String(), ReplacementEcosystem: ptr(string(pkg.HackagePkg))},
+ {Ecosystem: pkg.Java.String(), ReplacementEcosystem: ptr(string(pkg.JavaPkg))},
+ {Ecosystem: pkg.JavaScript.String(), ReplacementEcosystem: ptr(string(pkg.NpmPkg))},
+ {Ecosystem: pkg.Lua.String(), ReplacementEcosystem: ptr(string(pkg.LuaRocksPkg))},
+ {Ecosystem: pkg.OCaml.String(), ReplacementEcosystem: ptr(string(pkg.OpamPkg))},
+ {Ecosystem: pkg.PHP.String(), ReplacementEcosystem: ptr(string(pkg.PhpComposerPkg))},
+ {Ecosystem: pkg.Python.String(), ReplacementEcosystem: ptr(string(pkg.PythonPkg))},
+ {Ecosystem: pkg.R.String(), ReplacementEcosystem: ptr(string(pkg.Rpkg))},
+ {Ecosystem: pkg.Ruby.String(), ReplacementEcosystem: ptr(string(pkg.GemPkg))},
+ {Ecosystem: pkg.Rust.String(), ReplacementEcosystem: ptr(string(pkg.RustPkg))},
+ {Ecosystem: pkg.Swift.String(), ReplacementEcosystem: ptr(string(pkg.SwiftPkg))},
+ {Ecosystem: pkg.Swipl.String(), ReplacementEcosystem: ptr(string(pkg.SwiplPackPkg))},
+
+ // 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
+ for _, t := range pkg.AllPkgs {
+ // these types should never be mapped to
+ // jenkins plugin: java-archive supersedes this
+ // github action workflow: github-action supersedes this
+ switch t {
+ case pkg.JenkinsPluginPkg, pkg.GithubActionWorkflowPkg:
+ continue
+ }
+
+ purlType := t.PackageURLType()
+ if purlType == "" || purlType == string(t) || strings.HasPrefix(purlType, "generic") {
+ continue
+ }
+
+ ret = append(ret, PackageSpecifierOverride{
+ Ecosystem: purlType,
+ ReplacementEcosystem: ptr(string(t)),
+ })
+ }
+ return ret
+}
+
+func ptr[T any](v T) *T {
+ return &v
+}
diff --git a/grype/db/v6/db.go b/grype/db/v6/db.go
new file mode 100644
index 00000000000..002eb1c15b2
--- /dev/null
+++ b/grype/db/v6/db.go
@@ -0,0 +1,141 @@
+package v6
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "path/filepath"
+
+ "gorm.io/gorm"
+
+ "github.com/anchore/grype/grype/db/internal/gormadapter"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+)
+
+const (
+ // We follow SchemaVer semantics (see https://snowplow.io/blog/introducing-schemaver-for-semantic-versioning-of-schemas)
+
+ // ModelVersion indicates how many breaking schema changes there have been (which will prevent interaction with any historical data)
+ // note: this must ALWAYS be "6" in the context of this package.
+ ModelVersion = 6
+
+ // Revision indicates how many changes have been introduced which **may** prevent interaction with some historical data
+ Revision = 0
+
+ // Addition indicates how many changes have been introduced that are compatible with all historical data
+ Addition = 2
+
+ // v6 model changelog:
+ // 6.0.0: Initial version 🎉
+ // 6.0.1: Add CISA KEV to VulnerabilityDecorator store
+ // 6.0.2: Add EPSS to VulnerabilityDecorator store
+)
+
+const (
+ VulnerabilityDBFileName = "vulnerability.db"
+
+ // batchSize affects how many records are fetched at a time from the DB. Note: when using preload, row entries
+ // for related records may convey as parameters in a "WHERE x in (...)" which can lead to a large number of
+ // parameters in the query -- if above 999 then this will result in an error for sqlite. For this reason we
+ // try to keep this value well below 999.
+ batchSize = 300
+)
+
+var ErrDBCapabilityNotSupported = fmt.Errorf("capability not supported by DB")
+
+type ReadWriter interface {
+ Reader
+ Writer
+}
+
+type Reader interface {
+ DBMetadataStoreReader
+ ProviderStoreReader
+ VulnerabilityStoreReader
+ VulnerabilityDecoratorStoreReader
+ AffectedPackageStoreReader
+ AffectedCPEStoreReader
+ io.Closer
+ attachBlobValue(...blobable) error
+}
+
+type Writer interface {
+ DBMetadataStoreWriter
+ ProviderStoreWriter
+ VulnerabilityStoreWriter
+ VulnerabilityDecoratorStoreWriter
+ AffectedPackageStoreWriter
+ AffectedCPEStoreWriter
+ io.Closer
+}
+
+type Curator interface {
+ Reader() (Reader, error)
+ Status() vulnerability.ProviderStatus
+ Delete() error
+ Update() (bool, error)
+ Import(dbArchivePath string) error
+}
+
+type Config struct {
+ DBDirPath string
+ Debug bool
+}
+
+func (c Config) DBFilePath() string {
+ return filepath.Join(c.DBDirPath, VulnerabilityDBFileName)
+}
+
+func NewReader(cfg Config) (Reader, error) {
+ return newStore(cfg, false, false)
+}
+
+func NewWriter(cfg Config) (ReadWriter, error) {
+ return newStore(cfg, true, true)
+}
+
+func Hydrater() func(string) error {
+ return func(path string) error {
+ // this will auto-migrate any models, creating and populating indexes as needed
+ // 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)
+ if s != nil {
+ log.CloseAndLogError(s, path)
+ }
+ return err
+ }
+}
+
+// NewLowLevelDB creates a new empty DB for writing or opens an existing one for reading from the given path. This is
+// not recommended for typical interactions with the vulnerability DB, use NewReader and NewWriter instead.
+func NewLowLevelDB(dbFilePath string, empty, writable, debug bool) (*gorm.DB, error) {
+ opts := []gormadapter.Option{
+ gormadapter.WithDebug(debug),
+ }
+
+ if empty && !writable {
+ return nil, fmt.Errorf("cannot open an empty database for reading only")
+ }
+
+ if empty {
+ opts = append(opts,
+ gormadapter.WithTruncate(true, Models(), InitialData()),
+ )
+ } else if writable {
+ opts = append(opts, gormadapter.WithWritable(true, Models()))
+ }
+
+ dbObj, err := gormadapter.Open(dbFilePath, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ if empty {
+ // speed up writes by persisting key-to-ID lookups when writing to the DB
+ dbObj = dbObj.WithContext(withCacheContext(context.Background(), newCache()))
+ }
+
+ return dbObj, err
+}
diff --git a/grype/db/v6/db_metadata_store.go b/grype/db/v6/db_metadata_store.go
new file mode 100644
index 00000000000..b90269000cb
--- /dev/null
+++ b/grype/db/v6/db_metadata_store.go
@@ -0,0 +1,61 @@
+package v6
+
+import (
+ "fmt"
+ "time"
+
+ "gorm.io/gorm"
+
+ "github.com/anchore/grype/internal/log"
+)
+
+type DBMetadataStoreWriter interface {
+ SetDBMetadata() error
+}
+
+type DBMetadataStoreReader interface {
+ GetDBMetadata() (*DBMetadata, error)
+}
+
+type dbMetadataStore struct {
+ db *gorm.DB
+}
+
+func newDBMetadataStore(db *gorm.DB) *dbMetadataStore {
+ return &dbMetadataStore{
+ db: db,
+ }
+}
+
+func (s *dbMetadataStore) GetDBMetadata() (*DBMetadata, error) {
+ var model DBMetadata
+
+ result := s.db.First(&model)
+ return &model, result.Error
+}
+
+func (s *dbMetadataStore) SetDBMetadata() error {
+ log.Trace("writing DB metadata")
+
+ if err := s.db.Where("true").Delete(&DBMetadata{}).Error; err != nil {
+ return fmt.Errorf("failed to delete existing DB metadata record: %w", err)
+ }
+
+ // note: it is important to round the time to the second to avoid issues with the database update check.
+ // since we are comparing timestamps that are RFC3339 formatted, it's possible that milliseconds will
+ // be rounded up, causing a slight difference in candidate timestamps vs current DB timestamps.
+ ts := time.Now().UTC().Round(time.Second)
+
+ instance := &DBMetadata{
+ BuildTimestamp: &ts,
+ Model: ModelVersion,
+ Revision: Revision,
+ Addition: Addition,
+ }
+
+ if err := s.db.Create(instance).Error; err != nil {
+ return fmt.Errorf("failed to create DB metadata record: %w", err)
+ }
+
+ return nil
+}
diff --git a/grype/db/v6/db_metadata_store_test.go b/grype/db/v6/db_metadata_store_test.go
new file mode 100644
index 00000000000..048f007b26b
--- /dev/null
+++ b/grype/db/v6/db_metadata_store_test.go
@@ -0,0 +1,87 @@
+package v6
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "gorm.io/gorm"
+)
+
+func TestDbMetadataStore_empty(t *testing.T) {
+ db := setupTestStore(t).db
+ require.NoError(t, db.Where("true").Delete(&DBMetadata{}).Error) // delete all existing records
+ s := newDBMetadataStore(db)
+
+ // attempt to fetch a non-existent record
+ actualMetadata, err := s.GetDBMetadata()
+ require.ErrorIs(t, err, gorm.ErrRecordNotFound)
+ 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)
+
+ require.NoError(t, s.SetDBMetadata())
+
+ // fetch the record
+ actualMetadata, err := s.GetDBMetadata()
+ require.NoError(t, err)
+ require.NotNil(t, actualMetadata)
+
+ assert.NotZero(t, *actualMetadata.BuildTimestamp) // a timestamp was set
+ name, _ := actualMetadata.BuildTimestamp.Zone()
+ assert.Equal(t, "UTC", name) // the timestamp is in UTC
+
+ actualMetadata.BuildTimestamp = nil // value not under test
+
+ assert.Equal(t, DBMetadata{
+ BuildTimestamp: nil,
+ // expect the correct version info
+ Model: ModelVersion,
+ Revision: Revision,
+ Addition: Addition,
+ }, *actualMetadata)
+}
+
+func setupTestStore(t testing.TB, d ...string) *store {
+ var dir string
+ switch len(d) {
+ case 0:
+ dir = t.TempDir()
+ case 1:
+ dir = d[0]
+ default:
+ t.Fatal("too many arguments")
+
+ }
+
+ s, err := newStore(Config{
+ DBDirPath: dir,
+ }, true, true)
+ require.NoError(t, err)
+
+ require.NoError(t, s.SetDBMetadata())
+
+ return s
+}
+
+func setupReadOnlyTestStore(t testing.TB, dir string) *store {
+ s, err := newStore(Config{
+ DBDirPath: dir,
+ }, false, false)
+ require.NoError(t, err)
+
+ return s
+}
diff --git a/grype/db/v6/description.go b/grype/db/v6/description.go
new file mode 100644
index 00000000000..e72d016ab13
--- /dev/null
+++ b/grype/db/v6/description.go
@@ -0,0 +1,94 @@
+package v6
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+var ErrDBDoesNotExist = errors.New("database does not exist")
+
+type Description struct {
+ // SchemaVersion is the version of the DB schema
+ SchemaVersion schemaver.SchemaVer `json:"schemaVersion,omitempty"`
+
+ // Built is the timestamp the database was built
+ Built Time `json:"built"`
+}
+
+type Time struct {
+ time.Time
+}
+
+func (t Time) MarshalJSON() ([]byte, error) {
+ return []byte(fmt.Sprintf("%q", t.String())), nil
+}
+
+func (t *Time) UnmarshalJSON(data []byte) error {
+ str := string(data)
+ if len(str) < 2 || str[0] != '"' || str[len(str)-1] != '"' {
+ return fmt.Errorf("invalid time format")
+ }
+ str = str[1 : len(str)-1]
+
+ parsedTime, err := time.Parse(time.RFC3339, str)
+ if err != nil {
+ return err
+ }
+
+ t.Time = parsedTime.In(time.UTC)
+ return nil
+}
+
+func (t Time) String() string {
+ return t.Time.UTC().Round(time.Second).Format(time.RFC3339)
+}
+
+func DescriptionFromMetadata(m *DBMetadata) *Description {
+ if m == nil {
+ return nil
+ }
+ return &Description{
+ SchemaVersion: schemaver.New(m.Model, m.Revision, m.Addition),
+ Built: Time{Time: *m.BuildTimestamp},
+ }
+}
+
+func ReadDescription(dbFilePath string) (*Description, error) {
+ // check if exists
+ if _, err := os.Stat(dbFilePath); err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, ErrDBDoesNotExist
+ }
+ return nil, fmt.Errorf("failed to access database file: %w", err)
+ }
+
+ // access the DB to get the built time and schema version
+ r, err := NewReader(Config{
+ DBDirPath: filepath.Dir(dbFilePath),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to read DB description: %w", err)
+ }
+ // we need to ensure readers are closed, or we can see stale reads in new readers!
+ defer log.CloseAndLogError(r, dbFilePath)
+
+ meta, err := r.GetDBMetadata()
+ if err != nil {
+ return nil, fmt.Errorf("failed to read DB metadata: %w", err)
+ }
+
+ return &Description{
+ SchemaVersion: schemaver.New(meta.Model, meta.Revision, meta.Addition),
+ Built: Time{Time: *meta.BuildTimestamp},
+ }, nil
+}
+
+func (m Description) String() string {
+ return fmt.Sprintf("DB(version=%s built=%s)", m.SchemaVersion, m.Built)
+}
diff --git a/grype/db/v6/description_test.go b/grype/db/v6/description_test.go
new file mode 100644
index 00000000000..baacb262df1
--- /dev/null
+++ b/grype/db/v6/description_test.go
@@ -0,0 +1,111 @@
+package v6
+
+import (
+ "encoding/json"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+func TestReadDescription(t *testing.T) {
+ tempDir := t.TempDir()
+
+ s, err := NewWriter(Config{DBDirPath: tempDir})
+ require.NoError(t, err)
+ require.NoError(t, s.SetDBMetadata())
+ expected, err := s.GetDBMetadata()
+ require.NoError(t, err)
+ require.NoError(t, s.Close())
+
+ dbFilePath := filepath.Join(tempDir, VulnerabilityDBFileName)
+
+ description, err := ReadDescription(dbFilePath)
+ require.NoError(t, err)
+ require.NotNil(t, description)
+
+ assert.Equal(t, Description{
+ SchemaVersion: schemaver.New(expected.Model, expected.Revision, expected.Addition),
+ Built: Time{*expected.BuildTimestamp},
+ }, *description)
+}
+
+func TestTime_JSONMarshalling(t *testing.T) {
+ tests := []struct {
+ name string
+ time Time
+ expected string
+ }{
+ {
+ name: "go case",
+ time: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
+ expected: `"2023-09-26T12:00:00Z"`,
+ },
+ {
+ name: "convert to utc",
+ time: Time{time.Date(2023, 9, 26, 13, 0, 0, 0, time.FixedZone("UTC+1", 3600))},
+ expected: `"2023-09-26T12:00:00Z"`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ jsonData, err := json.Marshal(tt.time)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, string(jsonData))
+ })
+ }
+}
+
+func TestTime_JSONUnmarshalling(t *testing.T) {
+ tests := []struct {
+ name string
+ jsonData string
+ expectedTime Time
+ expectError require.ErrorAssertionFunc
+ }{
+ {
+ name: "use zulu offset",
+ jsonData: `"2023-09-26T12:00:00Z"`,
+ expectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
+ },
+ {
+ name: "use tz offset in another timezone",
+ jsonData: `"2023-09-26T14:00:00+02:00"`,
+ expectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
+ },
+ {
+ name: "use tz offset that is utc",
+ jsonData: `"2023-09-26T12:00:00+00:00"`,
+ expectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
+ },
+ {
+ name: "invalid format",
+ jsonData: `"invalid-time-format"`,
+ expectError: require.Error,
+ },
+ {
+ name: "invalid json",
+ jsonData: `invalid`,
+ expectError: require.Error,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectError == nil {
+ tt.expectError = require.NoError
+ }
+ var parsedTime Time
+ err := json.Unmarshal([]byte(tt.jsonData), &parsedTime)
+ tt.expectError(t, err)
+ if err == nil {
+ assert.Equal(t, tt.expectedTime.Time, parsedTime.Time)
+ }
+ })
+ }
+}
diff --git a/grype/db/v6/distribution/client.go b/grype/db/v6/distribution/client.go
new file mode 100644
index 00000000000..92ec2e4afb7
--- /dev/null
+++ b/grype/db/v6/distribution/client.go
@@ -0,0 +1,303 @@
+package distribution
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/go-cleanhttp"
+ "github.com/spf13/afero"
+ "github.com/wagoodman/go-progress"
+
+ "github.com/anchore/clio"
+ v6 "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/internal/bus"
+ "github.com/anchore/grype/internal/file"
+ "github.com/anchore/grype/internal/log"
+)
+
+type Config struct {
+ ID clio.Identification
+
+ // check/fetch parameters
+ LatestURL string
+ CACert string
+
+ // validations
+ RequireUpdateCheck bool
+
+ // timeouts
+ CheckTimeout time.Duration
+ UpdateTimeout time.Duration
+}
+
+type Client interface {
+ Latest() (*LatestDocument, error)
+ IsUpdateAvailable(current *v6.Description) (*Archive, error)
+ ResolveArchiveURL(archive Archive) (string, error)
+ Download(url, dest string, downloadProgress *progress.Manual) (string, error)
+}
+
+type client struct {
+ fs afero.Fs
+ dbDownloader file.Getter
+ listingDownloader file.Getter
+ config Config
+}
+
+func DefaultConfig() Config {
+ return Config{
+ LatestURL: "https://grype.anchore.io/databases",
+ RequireUpdateCheck: false,
+ CheckTimeout: 30 * time.Second,
+ UpdateTimeout: 300 * time.Second,
+ }
+}
+
+func NewClient(cfg Config) (Client, error) {
+ fs := afero.NewOsFs()
+ latestClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.CheckTimeout), withUserAgent(cfg.ID))
+ if err != nil {
+ return client{}, err
+ }
+
+ dbClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.UpdateTimeout), withUserAgent(cfg.ID))
+ if err != nil {
+ return client{}, err
+ }
+
+ return client{
+ fs: fs,
+ listingDownloader: file.NewGetter(cfg.ID, latestClient),
+ dbDownloader: file.NewGetter(cfg.ID, dbClient),
+ config: cfg,
+ }, nil
+}
+
+// IsUpdateAvailable indicates if there is a new update available as a boolean, and returns the latest db information
+// available for this schema.
+func (c client) IsUpdateAvailable(current *v6.Description) (*Archive, error) {
+ log.Debugf("checking for available database updates")
+
+ latestDoc, err := c.Latest()
+ if err != nil {
+ if c.config.RequireUpdateCheck {
+ return nil, fmt.Errorf("check for vulnerability database update failed: %+v", err)
+ }
+ log.Warnf("unable to check for vulnerability database update")
+ log.Debugf("check for vulnerability update failed: %+v", err)
+ }
+
+ archive, message := c.isUpdateAvailable(current, latestDoc)
+
+ if message != "" {
+ log.Warn(message)
+ bus.Notify(message)
+ }
+
+ return archive, err
+}
+
+func (c client) isUpdateAvailable(current *v6.Description, candidate *LatestDocument) (*Archive, string) {
+ if candidate == nil {
+ return nil, ""
+ }
+
+ var message string
+ switch candidate.Status {
+ case StatusDeprecated:
+ message = "this version of grype will soon stop receiving vulnerability database updates, please update grype"
+ case StatusEndOfLife:
+ message = "this version of grype is no longer receiving vulnerability database updates, please update grype"
+ }
+
+ // compare created data to current db date
+ if isSupersededBy(current, candidate.Description) {
+ log.Debugf("database update available: %s", candidate.Description)
+ return &candidate.Archive, message
+ }
+
+ log.Debugf("no database update available")
+ return nil, message
+}
+
+func (c client) ResolveArchiveURL(archive Archive) (string, error) {
+ // download the db to the temp dir
+ u, err := url.Parse(c.latestURL())
+ if err != nil {
+ return "", fmt.Errorf("unable to parse db URL %q: %w", c.latestURL(), err)
+ }
+
+ u.Path = path.Join(path.Dir(u.Path), path.Clean(archive.Path))
+
+ // from go-getter, adding a checksum as a query string will validate the payload after download
+ // note: the checksum query parameter is not sent to the server
+ query := u.Query()
+ if archive.Checksum != "" {
+ query.Add("checksum", archive.Checksum)
+ }
+ 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, archiveURL, downloadProgress)
+ if err != nil {
+ removeAllOrLog(afero.NewOsFs(), tempDir)
+ return "", fmt.Errorf("unable to download db: %w", err)
+ }
+
+ return tempDir, nil
+}
+
+// Latest loads a LatestDocument from the configured URL.
+func (c client) Latest() (*LatestDocument, error) {
+ tempFile, err := afero.TempFile(c.fs, "", "grype-db-listing")
+ if err != nil {
+ 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")
+ }
+ }()
+
+ err = c.listingDownloader.GetFile(tempFile.Name(), c.latestURL())
+ if err != nil {
+ return nil, fmt.Errorf("unable to download listing: %w", err)
+ }
+
+ return NewLatestFromFile(c.fs, tempFile.Name())
+}
+
+func (c client) latestURL() string {
+ u := c.config.LatestURL
+ // allow path to be specified directly to a json file, or the path without version information
+ if !strings.HasSuffix(u, ".json") {
+ u = strings.TrimRight(u, "/")
+ u = fmt.Sprintf("%s/v%d/%s", u, v6.ModelVersion, LatestFileName)
+ }
+ return u
+}
+
+func withClientTimeout(timeout time.Duration) func(*http.Client) {
+ return func(c *http.Client) {
+ c.Timeout = timeout
+ }
+}
+
+func withUserAgent(id clio.Identification) func(*http.Client) {
+ return func(c *http.Client) {
+ *(c) = *newHTTPClientWithDefaultUserAgent(c.Transport, fmt.Sprintf("%s %s", id.Name, id.Version))
+ }
+}
+
+func defaultHTTPClient(fs afero.Fs, caCertPath string, postProcessor ...func(*http.Client)) (*http.Client, error) {
+ httpClient := cleanhttp.DefaultClient()
+ httpClient.Timeout = 30 * time.Second
+ if caCertPath != "" {
+ rootCAs := x509.NewCertPool()
+
+ pemBytes, err := afero.ReadFile(fs, caCertPath)
+ if err != nil {
+ return nil, fmt.Errorf("unable to configure root CAs for curator: %w", err)
+ }
+ rootCAs.AppendCertsFromPEM(pemBytes)
+
+ httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
+ MinVersion: tls.VersionTLS12,
+ RootCAs: rootCAs,
+ }
+ }
+
+ for _, pp := range postProcessor {
+ pp(httpClient)
+ }
+
+ return httpClient, nil
+}
+
+func removeAllOrLog(fs afero.Fs, dir string) {
+ if err := fs.RemoveAll(dir); err != nil {
+ log.WithFields("error", err).Warnf("failed to remove path %q", dir)
+ }
+}
+
+func isSupersededBy(current *v6.Description, candidate v6.Description) bool {
+ if current == nil {
+ log.Debug("cannot find existing metadata, using update...")
+ // any valid update beats no database, use it!
+ return true
+ }
+
+ if !current.SchemaVersion.Valid() {
+ log.Error("existing database has no schema version, doing nothing...")
+ return false
+ }
+
+ if !candidate.SchemaVersion.Valid() {
+ log.Error("update has no schema version, doing nothing...")
+ return false
+ }
+
+ if candidate.SchemaVersion.Model != current.SchemaVersion.Model {
+ log.WithFields("want", current.SchemaVersion.Model, "received", candidate.SchemaVersion.Model).Warn("update is for a different DB schema, skipping...")
+ return false
+ }
+
+ if candidate.Built.After(current.Built.Time) {
+ d := candidate.Built.Sub(current.Built.Time).String()
+ log.WithFields("existing", current.Built.String(), "candidate", candidate.Built.String(), "delta", d).Debug("existing database is older than candidate update, using update...")
+ // the listing is newer than the existing db, use it!
+ return true
+ }
+
+ log.Debugf("existing database is already up to date")
+ return false
+}
+
+func newHTTPClientWithDefaultUserAgent(baseTransport http.RoundTripper, userAgent string) *http.Client {
+ return &http.Client{
+ Transport: roundTripperWithUserAgent{
+ transport: baseTransport,
+ userAgent: userAgent,
+ },
+ }
+}
+
+type roundTripperWithUserAgent struct {
+ transport http.RoundTripper
+ userAgent string
+}
+
+func (r roundTripperWithUserAgent) RoundTrip(req *http.Request) (*http.Response, error) {
+ clonedReq := req.Clone(req.Context())
+
+ if clonedReq.Header.Get("User-Agent") == "" {
+ clonedReq.Header.Set("User-Agent", r.userAgent)
+ }
+
+ return r.transport.RoundTrip(clonedReq)
+}
diff --git a/grype/db/v6/distribution/client_test.go b/grype/db/v6/distribution/client_test.go
new file mode 100644
index 00000000000..f8f72bbf1b3
--- /dev/null
+++ b/grype/db/v6/distribution/client_test.go
@@ -0,0 +1,418 @@
+package distribution
+
+import (
+ "encoding/json"
+ "errors"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+ "github.com/wagoodman/go-progress"
+
+ db "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+type mockGetter struct {
+ mock.Mock
+}
+
+func (m *mockGetter) GetFile(dst, src string, manuals ...*progress.Manual) error {
+ args := m.Called(dst, src, manuals)
+ return args.Error(0)
+}
+
+func (m *mockGetter) GetToDir(dst, src string, manuals ...*progress.Manual) error {
+ args := m.Called(dst, src, manuals)
+ return args.Error(0)
+}
+
+func TestClient_Latest(t *testing.T) {
+ tests := []struct {
+ name string
+ latestResponse []byte
+ getFileErr error
+ expectedDoc *LatestDocument
+ expectedErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "go case",
+ latestResponse: func() []byte {
+ doc := LatestDocument{
+ Status: "active",
+ Archive: Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "path/to/archive",
+ Checksum: "checksum123",
+ },
+ }
+ data, err := json.Marshal(doc)
+ require.NoError(t, err)
+ return data
+ }(),
+ expectedDoc: &LatestDocument{
+ Status: "active",
+ Archive: Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "path/to/archive",
+ Checksum: "checksum123",
+ },
+ },
+ },
+ {
+ name: "download error",
+ getFileErr: errors.New("failed to download file"),
+ expectedDoc: nil,
+ expectedErr: func(t require.TestingT, err error, _ ...interface{}) {
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unable to download listing")
+ },
+ },
+ {
+ name: "malformed JSON response",
+ latestResponse: []byte("malformed json"),
+ expectedDoc: nil,
+ expectedErr: func(t require.TestingT, err error, _ ...interface{}) {
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "invalid character 'm' looking for beginning of value")
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectedErr == nil {
+ tt.expectedErr = require.NoError
+ }
+ mockFs := afero.NewMemMapFs()
+
+ mg := new(mockGetter)
+
+ mg.On("GetFile", mock.Anything, "http://localhost:8080/latest.json", mock.Anything).Run(func(args mock.Arguments) {
+ if tt.getFileErr != nil {
+ return
+ }
+
+ dst := args.String(0)
+ err := afero.WriteFile(mockFs, dst, tt.latestResponse, 0644)
+ require.NoError(t, err)
+ }).Return(tt.getFileErr)
+
+ c, err := NewClient(Config{
+ LatestURL: "http://localhost:8080/latest.json",
+ })
+ require.NoError(t, err)
+
+ cl := c.(client)
+ cl.fs = mockFs
+ cl.listingDownloader = mg
+
+ doc, err := cl.Latest()
+ tt.expectedErr(t, err)
+ if err != nil {
+ return
+ }
+
+ require.Equal(t, tt.expectedDoc, doc)
+ mg.AssertExpectations(t)
+ })
+ }
+}
+
+func TestClient_Download(t *testing.T) {
+ destDir := t.TempDir()
+
+ setup := func() (Client, *mockGetter) {
+ mg := new(mockGetter)
+
+ c, err := NewClient(Config{
+ LatestURL: "http://localhost:8080/latest.json",
+ })
+ require.NoError(t, err)
+
+ cl := c.(client)
+ cl.dbDownloader = mg
+
+ return cl, mg
+ }
+
+ t.Run("successful download", func(t *testing.T) {
+ c, mg := setup()
+ 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(url, destDir, &progress.Manual{})
+ require.NoError(t, err)
+ require.True(t, len(tempDir) > 0)
+
+ mg.AssertExpectations(t)
+ })
+
+ t.Run("download error", func(t *testing.T) {
+ c, mg := setup()
+ 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(url, destDir, &progress.Manual{})
+ require.Error(t, err)
+ require.Empty(t, tempDir)
+ require.Contains(t, err.Error(), "unable to download db")
+
+ mg.AssertExpectations(t)
+ })
+
+ t.Run("nested into dir that does not exist", func(t *testing.T) {
+ c, mg := setup()
+ 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(url, nestedPath, &progress.Manual{})
+ require.NoError(t, err)
+ require.True(t, len(tempDir) > 0)
+
+ mg.AssertExpectations(t)
+ })
+}
+
+func TestClient_IsUpdateAvailable(t *testing.T) {
+ current := &db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
+ }
+
+ tests := []struct {
+ name string
+ candidate *LatestDocument
+ archive *Archive
+ message string
+ }{
+ {
+ name: "update available",
+ candidate: &LatestDocument{
+ Status: StatusActive,
+ Archive: Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "path/to/archive.tar.gz",
+ Checksum: "checksum123",
+ },
+ },
+ archive: &Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "path/to/archive.tar.gz",
+ Checksum: "checksum123",
+ },
+ },
+ {
+ name: "no update available",
+ candidate: &LatestDocument{
+ Status: "active",
+ Archive: Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "path/to/archive.tar.gz",
+ Checksum: "checksum123",
+ },
+ },
+ archive: nil,
+ },
+ {
+ name: "no candidate available",
+ candidate: nil,
+ archive: nil,
+ },
+ {
+ name: "candidate deprecated",
+ candidate: &LatestDocument{
+ Status: StatusDeprecated,
+ Archive: Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "path/to/archive.tar.gz",
+ Checksum: "checksum123",
+ },
+ },
+ archive: &Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "path/to/archive.tar.gz",
+ Checksum: "checksum123",
+ },
+ message: "this version of grype will soon stop receiving vulnerability database updates, please update grype",
+ },
+ {
+ name: "candidate end of life",
+ candidate: &LatestDocument{
+ Status: StatusEndOfLife,
+ Archive: Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "path/to/archive.tar.gz",
+ Checksum: "checksum123",
+ },
+ },
+ archive: &Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "path/to/archive.tar.gz",
+ Checksum: "checksum123",
+ },
+ message: "this version of grype is no longer receiving vulnerability database updates, please update grype",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c, err := NewClient(Config{})
+ require.NoError(t, err)
+
+ cl := c.(client)
+
+ archive, message := cl.isUpdateAvailable(current, tt.candidate)
+ assert.Equal(t, tt.message, message)
+ assert.Equal(t, tt.archive, archive)
+ })
+ }
+}
+
+func TestDatabaseDescription_IsSupersededBy(t *testing.T) {
+ t1 := time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)
+ t2 := time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)
+
+ currentMetadata := db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: t1},
+ }
+
+ newerMetadata := db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: t2},
+ }
+
+ olderMetadata := db.Description{
+ SchemaVersion: schemaver.New(1, 0, 0),
+ Built: db.Time{Time: t1},
+ }
+
+ differentModelMetadata := db.Description{
+ SchemaVersion: schemaver.New(2, 0, 0),
+ Built: db.Time{Time: t2},
+ }
+
+ tests := []struct {
+ name string
+ current *db.Description
+ other db.Description
+ expected bool
+ }{
+ {
+ name: "no current metadata",
+ current: nil,
+ other: newerMetadata,
+ expected: true,
+ },
+ {
+ name: "newer build",
+ current: ¤tMetadata,
+ other: newerMetadata,
+ expected: true,
+ },
+ {
+ name: "older build",
+ current: ¤tMetadata,
+ other: olderMetadata,
+ expected: false,
+ },
+ {
+ name: "different schema version",
+ current: ¤tMetadata,
+ other: differentModelMetadata,
+ expected: false,
+ },
+ {
+ name: "current metadata has no schema version",
+ current: &db.Description{Built: db.Time{Time: t1}},
+ other: newerMetadata,
+ expected: false,
+ },
+ {
+ name: "update has no schema version",
+ current: ¤tMetadata,
+ other: db.Description{Built: db.Time{Time: t2}},
+ expected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := isSupersededBy(tt.current, tt.other)
+ require.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+func Test_latestURL(t *testing.T) {
+ tests := []struct {
+ url string
+ expected string
+ }{
+ {
+ url: "https://grype.anchore.io/databases",
+ expected: "https://grype.anchore.io/databases/v6/latest.json",
+ },
+ {
+ url: "https://grype.anchore.io/databases/",
+ expected: "https://grype.anchore.io/databases/v6/latest.json",
+ },
+ {
+ url: "https://grype.anchore.io/databases/v6/latest.json",
+ expected: "https://grype.anchore.io/databases/v6/latest.json",
+ },
+ {
+ url: "http://grype.anchore.io/databases/",
+ expected: "http://grype.anchore.io/databases/v6/latest.json",
+ },
+ {
+ url: "https://example.com/file.json",
+ expected: "https://example.com/file.json",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.url, func(t *testing.T) {
+ c := client{
+ config: Config{
+ LatestURL: test.url,
+ },
+ }
+ got := c.latestURL()
+ require.Equal(t, test.expected, got)
+ })
+ }
+}
diff --git a/grype/db/v6/distribution/latest.go b/grype/db/v6/distribution/latest.go
new file mode 100644
index 00000000000..fb9e5a1a646
--- /dev/null
+++ b/grype/db/v6/distribution/latest.go
@@ -0,0 +1,139 @@
+package distribution
+
+import (
+ "crypto/sha256"
+ "encoding/json"
+ "fmt"
+ "io"
+ "path/filepath"
+ "sort"
+ "time"
+
+ "github.com/spf13/afero"
+
+ db "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/internal/file"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+const LatestFileName = "latest.json"
+
+type LatestDocument struct {
+ // Status indicates if the database is actively being maintained and distributed
+ Status Status `json:"status"`
+
+ // Archive is the most recent database that has been built and distributed, additionally annotated with provider-level information
+ Archive `json:",inline"`
+}
+
+type Archive struct {
+ // Description contains details about the database contained within the distribution archive
+ db.Description `json:",inline"`
+
+ // Path is the path to a DB archive relative to the listing file hosted location.
+ // Note: this is NOT the absolute URL to download the database.
+ Path string `json:"path"`
+
+ // Checksum is the self describing digest of the database archive referenced in path
+ Checksum string `json:"checksum"`
+}
+
+func NewLatestDocument(entries ...Archive) *LatestDocument {
+ var validEntries []Archive
+ for _, entry := range entries {
+ if entry.SchemaVersion.Model == db.ModelVersion {
+ validEntries = append(validEntries, entry)
+ }
+ }
+
+ if len(validEntries) == 0 {
+ return nil
+ }
+
+ // sort from most recent to the least recent
+ sort.SliceStable(validEntries, func(i, j int) bool {
+ return validEntries[i].Built.After(entries[j].Built.Time)
+ })
+
+ return &LatestDocument{
+ Archive: validEntries[0],
+ Status: LifecycleStatus,
+ }
+}
+
+func NewLatestFromReader(reader io.Reader) (*LatestDocument, error) {
+ var l LatestDocument
+ if err := json.NewDecoder(reader).Decode(&l); err != nil {
+ return nil, fmt.Errorf("unable to parse DB latest.json: %w", err)
+ }
+
+ if l == (LatestDocument{}) {
+ return nil, nil
+ }
+
+ return &l, nil
+}
+
+func NewLatestFromFile(fs afero.Fs, path string) (*LatestDocument, error) {
+ fh, err := fs.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read listing file: %w", err)
+ }
+ defer fh.Close()
+ return NewLatestFromReader(fh)
+}
+
+func NewArchive(path string, t time.Time, model, revision, addition int) (*Archive, error) {
+ checksum, err := calculateArchiveDigest(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to calculate archive checksum: %w", err)
+ }
+
+ return &Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(model, revision, addition),
+ Built: db.Time{Time: t},
+ },
+ // this is not the path on disk, this is the path relative to the latest.json file when hosted
+ Path: filepath.Base(path),
+ Checksum: checksum,
+ }, nil
+}
+
+func (l LatestDocument) Write(writer io.Writer) error {
+ if l.SchemaVersion.Model == 0 {
+ return fmt.Errorf("missing schema version")
+ }
+
+ if l.Status == "" {
+ l.Status = LifecycleStatus
+ }
+
+ if l.Path == "" {
+ return fmt.Errorf("missing archive path")
+ }
+
+ if l.Checksum == "" {
+ return fmt.Errorf("missing archive checksum")
+ }
+
+ if l.Built.IsZero() {
+ return fmt.Errorf("missing built time")
+ }
+
+ contents, err := json.MarshalIndent(&l, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to encode listing file: %w", err)
+ }
+
+ _, err = writer.Write(contents)
+ return err
+}
+
+func calculateArchiveDigest(dbFilePath string) (string, error) {
+ digest, err := file.HashFile(afero.NewOsFs(), dbFilePath, sha256.New())
+ if err != nil {
+ return "", fmt.Errorf("failed to calculate checksum for DB archive file: %w", err)
+ }
+ return fmt.Sprintf("sha256:%s", digest), nil
+}
diff --git a/grype/db/v6/distribution/latest_test.go b/grype/db/v6/distribution/latest_test.go
new file mode 100644
index 00000000000..9e09392bec6
--- /dev/null
+++ b/grype/db/v6/distribution/latest_test.go
@@ -0,0 +1,285 @@
+package distribution
+
+import (
+ "bytes"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ db "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+func TestNewLatestDocument(t *testing.T) {
+ t.Run("valid entries", func(t *testing.T) {
+ archive1 := Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ Built: db.Time{Time: time.Now()},
+ },
+ }
+ archive2 := Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ Built: db.Time{Time: time.Now().Add(-1 * time.Hour)},
+ },
+ }
+
+ latestDoc := NewLatestDocument(archive1, archive2)
+ require.NotNil(t, latestDoc)
+ require.Equal(t, latestDoc.Archive, archive1) // most recent archive
+ require.Equal(t, latestDoc.SchemaVersion.Model, db.ModelVersion)
+ })
+
+ t.Run("filter entries", func(t *testing.T) {
+ archive1 := Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(5, db.Revision, db.Addition), // old!
+ Built: db.Time{Time: time.Now()},
+ },
+ }
+ archive2 := Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ Built: db.Time{Time: time.Now().Add(-1 * time.Hour)},
+ },
+ }
+
+ latestDoc := NewLatestDocument(archive1, archive2)
+ require.NotNil(t, latestDoc)
+ require.Equal(t, latestDoc.Archive, archive2) // most recent archive with valid version
+ require.Equal(t, latestDoc.SchemaVersion.Model, db.ModelVersion)
+ })
+
+ t.Run("no entries", func(t *testing.T) {
+ latestDoc := NewLatestDocument()
+ require.Nil(t, latestDoc)
+ })
+}
+
+func TestNewLatestFromReader(t *testing.T) {
+
+ t.Run("valid JSON", func(t *testing.T) {
+ latestDoc := LatestDocument{
+ Archive: Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ Built: db.Time{Time: time.Now().Truncate(time.Second).UTC()},
+ },
+ },
+ Status: "active",
+ }
+
+ var buf bytes.Buffer
+ require.NoError(t, json.NewEncoder(&buf).Encode(latestDoc))
+
+ result, err := NewLatestFromReader(&buf)
+ require.NoError(t, err)
+ require.Equal(t, latestDoc.SchemaVersion, result.SchemaVersion)
+ require.Equal(t, latestDoc.Archive.Description.Built.Time, result.Archive.Description.Built.Time)
+ })
+
+ t.Run("empty", func(t *testing.T) {
+ emptyJSON := []byte("{}")
+ val, err := NewLatestFromReader(bytes.NewReader(emptyJSON))
+ require.NoError(t, err)
+ assert.Nil(t, val)
+ })
+
+ t.Run("invalid JSON", func(t *testing.T) {
+ invalidJSON := []byte("invalid json")
+ val, err := NewLatestFromReader(bytes.NewReader(invalidJSON))
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unable to parse DB latest.json")
+ assert.Nil(t, val)
+ })
+}
+
+func TestLatestDocument_Write(t *testing.T) {
+
+ errContains := func(text string) require.ErrorAssertionFunc {
+ return func(t require.TestingT, err error, msgAndArgs ...interface{}) {
+ require.ErrorContains(t, err, text, msgAndArgs...)
+ }
+ }
+
+ now := db.Time{Time: time.Now().Truncate(time.Second).UTC()}
+
+ tests := []struct {
+ name string
+ latestDoc LatestDocument
+ expectedError require.ErrorAssertionFunc
+ }{
+ {
+ name: "valid document",
+ latestDoc: LatestDocument{
+ Archive: Archive{
+ Description: db.Description{
+ Built: now,
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ },
+ Path: "valid/path/to/archive",
+ Checksum: "sha256:validchecksum",
+ },
+ // note: status not supplied, should assume to be active
+ },
+ expectedError: require.NoError,
+ },
+ {
+ name: "explicit status",
+ latestDoc: LatestDocument{
+ Archive: Archive{
+ Description: db.Description{
+ Built: now,
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ },
+ Path: "valid/path/to/archive",
+ Checksum: "xxh64:validchecksum",
+ },
+ Status: StatusDeprecated,
+ },
+ expectedError: require.NoError,
+ },
+ {
+ name: "missing schema version",
+ latestDoc: LatestDocument{
+ Archive: Archive{
+ Description: db.Description{
+ Built: now,
+ },
+ Path: "valid/path/to/archive",
+ Checksum: "xxh64:validchecksum",
+ },
+ Status: "active",
+ },
+ expectedError: errContains("missing schema version"),
+ },
+ {
+ name: "missing archive path",
+ latestDoc: LatestDocument{
+ Archive: Archive{
+ Description: db.Description{
+ Built: now,
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ },
+ Path: "", // this!
+ Checksum: "xxh64:validchecksum",
+ },
+ Status: "active",
+ },
+ expectedError: errContains("missing archive path"),
+ },
+ {
+ name: "missing archive checksum",
+ latestDoc: LatestDocument{
+ Archive: Archive{
+ Description: db.Description{
+ Built: now,
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ },
+ Path: "valid/path/to/archive",
+ Checksum: "", // this!
+ },
+ Status: "active",
+ },
+ expectedError: errContains("missing archive checksum"),
+ },
+ {
+ name: "missing built time",
+ latestDoc: LatestDocument{
+ Archive: Archive{
+ Description: db.Description{
+ Built: db.Time{}, // this!
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ },
+ Path: "valid/path/to/archive",
+ Checksum: "xxh64:validchecksum",
+ },
+ Status: "active",
+ },
+ expectedError: errContains("missing built time"),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectedError == nil {
+ tt.expectedError = require.NoError
+ }
+ var buf bytes.Buffer
+ err := tt.latestDoc.Write(&buf)
+ tt.expectedError(t, err)
+ if err != nil {
+ return
+ }
+
+ var result LatestDocument
+ assert.NoError(t, json.Unmarshal(buf.Bytes(), &result))
+ assert.Equal(t, tt.latestDoc.SchemaVersion, result.SchemaVersion, "schema version mismatch")
+ assert.Equal(t, tt.latestDoc.Archive.Checksum, result.Archive.Checksum, "archive checksum mismatch")
+ assert.Equal(t, tt.latestDoc.Archive.Description.Built.Time, result.Archive.Description.Built.Time, "built time mismatch")
+ assert.Equal(t, tt.latestDoc.Archive.Path, result.Archive.Path, "path mismatch")
+ if tt.latestDoc.Status == "" {
+ assert.Equal(t, StatusActive, result.Status, "status mismatch")
+ } else {
+ assert.Equal(t, tt.latestDoc.Status, result.Status, "status mismatch")
+ }
+ })
+ }
+}
+
+func TestNewArchive(t *testing.T) {
+ tests := []struct {
+ name string
+ contents string
+ time time.Time
+ model int
+ revision int
+ addition int
+ expectErr require.ErrorAssertionFunc
+ expected *Archive
+ }{
+ {
+ name: "valid input",
+ contents: "test archive content",
+ time: time.Date(2023, 11, 24, 12, 0, 0, 0, time.UTC),
+ model: 1,
+ revision: 0,
+ addition: 5,
+ expectErr: require.NoError,
+ expected: &Archive{
+ Description: db.Description{
+ SchemaVersion: schemaver.New(1, 0, 5),
+ Built: db.Time{Time: time.Date(2023, 11, 24, 12, 0, 0, 0, time.UTC)},
+ },
+ Path: "archive.tar.gz",
+ Checksum: "sha256:2a11c11d2c3803697c458a1f5f03c2b73235c101f93c88193cc8810003c40d87",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ d := t.TempDir()
+ tempFile, err := os.Create(filepath.Join(d, tt.expected.Path))
+ require.NoError(t, err)
+ _, err = tempFile.WriteString(tt.contents)
+ require.NoError(t, err)
+
+ archive, err := NewArchive(tempFile.Name(), tt.time, tt.model, tt.revision, tt.addition)
+ tt.expectErr(t, err)
+ if err != nil {
+ return
+ }
+ if diff := cmp.Diff(tt.expected, archive); diff != "" {
+ t.Errorf("unexpected archive (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/grype/db/v6/distribution/status.go b/grype/db/v6/distribution/status.go
new file mode 100644
index 00000000000..2f866d951b7
--- /dev/null
+++ b/grype/db/v6/distribution/status.go
@@ -0,0 +1,16 @@
+package distribution
+
+type Status string
+
+const LifecycleStatus = StatusActive
+
+const (
+ // StatusActive indicates the database is actively being maintained and distributed
+ StatusActive Status = "active"
+
+ // StatusDeprecated indicates the database is still being distributed but is approaching end of life. Upgrade grype to avoid future disruptions.
+ StatusDeprecated Status = "deprecated"
+
+ // StatusEndOfLife indicates the database is no longer being distributed. Users must build their own databases or upgrade grype.
+ StatusEndOfLife Status = "eol"
+)
diff --git a/grype/db/v6/enumerations.go b/grype/db/v6/enumerations.go
new file mode 100644
index 00000000000..cc7257e3b2d
--- /dev/null
+++ b/grype/db/v6/enumerations.go
@@ -0,0 +1,126 @@
+package v6
+
+import "strings"
+
+// VulnerabilityStatus is meant to convey the current point in the lifecycle for a vulnerability record.
+// This is roughly based on CVE status, NVD status, and vendor-specific status values (see https://nvd.nist.gov/vuln/vulnerability-status)
+type VulnerabilityStatus string
+
+const (
+ UnknownVulnerabilityStatus VulnerabilityStatus = ""
+
+ // VulnerabilityActive means that the information from the vulnerability record is actionable
+ VulnerabilityActive VulnerabilityStatus = "active" // empty also means active
+
+ // VulnerabilityAnalyzing means that the vulnerability record is being reviewed, it may or may not be actionable
+ VulnerabilityAnalyzing VulnerabilityStatus = "analyzing"
+
+ // VulnerabilityRejected means that data from the vulnerability record should not be acted upon
+ VulnerabilityRejected VulnerabilityStatus = "rejected"
+
+ // VulnerabilityDisputed means that the vulnerability record is in contention, it may or may not be actionable
+ VulnerabilityDisputed VulnerabilityStatus = "disputed"
+)
+
+// SeverityScheme represents how to interpret the string value for a vulnerability severity
+type SeverityScheme string
+
+const (
+ UnknownSeverityScheme SeverityScheme = ""
+
+ // SeveritySchemeCVSS is the Common Vulnerability Scoring System severity scheme
+ SeveritySchemeCVSS SeverityScheme = "CVSS"
+
+ // SeveritySchemeHML is a string severity scheme (High, Medium, Low)
+ SeveritySchemeHML SeverityScheme = "HML"
+
+ // SeveritySchemeCHML is a string severity scheme (Critical, High, Medium, Low)
+ SeveritySchemeCHML SeverityScheme = "CHML"
+
+ // SeveritySchemeCHMLN is a string severity scheme (Critical, High, Medium, Low, Negligible)
+ SeveritySchemeCHMLN SeverityScheme = "CHMLN"
+)
+
+// FixStatus conveys if the package is affected (or not) and the current availability (or not) of a fix
+type FixStatus string
+
+const (
+ UnknownFixStatus FixStatus = ""
+
+ // FixedStatus affirms the package is affected and a fix is available
+ FixedStatus FixStatus = "fixed"
+
+ // NotFixedStatus affirms the package is affected and a fix is not available
+ NotFixedStatus FixStatus = "not-fixed"
+
+ // WontFixStatus affirms the package is affected and a fix will not be provided
+ WontFixStatus FixStatus = "wont-fix"
+
+ // NotAffectedFixStatus affirms the package is not affected by the vulnerability
+ NotAffectedFixStatus FixStatus = "not-affected"
+)
+
+const (
+ // AdvisoryReferenceTag is a tag that can be used to identify vulnerability advisory URL references
+ AdvisoryReferenceTag = "advisory"
+)
+
+func ParseVulnerabilityStatus(s string) VulnerabilityStatus {
+ switch strings.TrimSpace(strings.ToLower(s)) {
+ case string(VulnerabilityActive), "":
+ return VulnerabilityActive
+ case string(VulnerabilityAnalyzing):
+ return VulnerabilityAnalyzing
+ case string(VulnerabilityRejected):
+ return VulnerabilityRejected
+ case string(VulnerabilityDisputed):
+ return VulnerabilityDisputed
+ default:
+ return UnknownVulnerabilityStatus
+ }
+}
+
+func ParseSeverityScheme(s string) SeverityScheme {
+ switch replaceAny(strings.TrimSpace(strings.ToLower(s)), "", "-", "_", " ") {
+ case strings.ToLower(string(SeveritySchemeCVSS)):
+ return SeveritySchemeCVSS
+ case strings.ToLower(string(SeveritySchemeHML)):
+ return SeveritySchemeHML
+ case strings.ToLower(string(SeveritySchemeCHML)):
+ return SeveritySchemeCHML
+ case strings.ToLower(string(SeveritySchemeCHMLN)):
+ return SeveritySchemeCHMLN
+ default:
+ return UnknownSeverityScheme
+ }
+}
+
+func ParseFixStatus(s string) FixStatus {
+ switch replaceAny(strings.TrimSpace(strings.ToLower(s)), "-", " ", "_") {
+ case string(FixedStatus):
+ return FixedStatus
+ case string(NotFixedStatus):
+ return NotFixedStatus
+ case string(WontFixStatus):
+ return WontFixStatus
+ case string(NotAffectedFixStatus):
+ return NotAffectedFixStatus
+ default:
+ return UnknownFixStatus
+ }
+}
+
+func NormalizeReferenceTags(tags []string) []string {
+ var normalized []string
+ for _, tag := range tags {
+ normalized = append(normalized, replaceAny(strings.ToLower(strings.TrimSpace(tag)), "-", " ", "_"))
+ }
+ return normalized
+}
+
+func replaceAny(input string, newStr string, searchFor ...string) string {
+ for _, s := range searchFor {
+ input = strings.ReplaceAll(input, s, newStr)
+ }
+ return input
+}
diff --git a/grype/db/v6/enumerations_test.go b/grype/db/v6/enumerations_test.go
new file mode 100644
index 00000000000..304db01051e
--- /dev/null
+++ b/grype/db/v6/enumerations_test.go
@@ -0,0 +1,86 @@
+package v6
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseVulnerabilityStatus(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected VulnerabilityStatus
+ }{
+ {"Active status", "active", VulnerabilityActive},
+ {"Analyzing status with whitespace", " analyzing ", VulnerabilityAnalyzing},
+ {"Rejected status in uppercase", "REJECTED", VulnerabilityRejected},
+ {"Disputed status", "disputed", VulnerabilityDisputed},
+ {"Unknown status", "unknown", UnknownVulnerabilityStatus},
+ {"Empty string as active status", "", VulnerabilityActive},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expected, ParseVulnerabilityStatus(tt.input))
+ })
+ }
+}
+
+func TestParseSeverityScheme(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected SeverityScheme
+ }{
+ {"CVSS scheme", "Cvss", SeveritySchemeCVSS},
+ {"HML scheme", "H-M-l", SeveritySchemeHML},
+ {"CHML scheme", "ChmL", SeveritySchemeCHML},
+ {"CHMLN scheme", "CHmLN", SeveritySchemeCHMLN},
+ {"Unknown scheme", "unknown", UnknownSeverityScheme},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expected, ParseSeverityScheme(tt.input))
+ })
+ }
+}
+
+func TestParseFixStatus(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected FixStatus
+ }{
+ {"Fixed status", "fixed", FixedStatus},
+ {"Not fixed status with hyphen", "not-fixed", NotFixedStatus},
+ {"Wont fix status in uppercase with underscore", "WONT_FIX", WontFixStatus},
+ {"Not affected status with whitespace", " not affected ", NotAffectedFixStatus},
+ {"Unknown status", "unknown", UnknownFixStatus},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expected, ParseFixStatus(tt.input))
+ })
+ }
+}
+
+func TestReplaceAny(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ newStr string
+ searchFor []string
+ expected string
+ }{
+ {"go case", "really not_fixed-i'promise", "-", []string{"'", " ", "_"}, "really-not-fixed-i-promise"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expected, replaceAny(tt.input, tt.newStr, tt.searchFor...))
+ })
+ }
+}
diff --git a/grype/db/v6/fillers.go b/grype/db/v6/fillers.go
new file mode 100644
index 00000000000..9998e1335e3
--- /dev/null
+++ b/grype/db/v6/fillers.go
@@ -0,0 +1,105 @@
+package v6
+
+import (
+ "errors"
+)
+
+// fillAffectedPackageHandles lazy loads all properties on the list of AffectedPackageHandles
+func fillAffectedPackageHandles(reader Reader, handles []*AffectedPackageHandle) error {
+ return errors.Join(
+ reader.attachBlobValue(toBlobables(handles)...),
+ fillRefs(reader, handles, affectedPackageHandleOperatingSystemRef, operatingSystemID),
+ fillRefs(reader, handles, affectedPackageHandlePackageRef, packageID),
+ fillVulnerabilityHandles(reader, handles, affectedPackageHandleVulnerabilityHandleRef),
+ )
+}
+
+func affectedPackageHandleOperatingSystemRef(t *AffectedPackageHandle) idRef[OperatingSystem] {
+ return idRef[OperatingSystem]{
+ id: t.OperatingSystemID,
+ ref: &t.OperatingSystem,
+ }
+}
+
+func affectedPackageHandlePackageRef(t *AffectedPackageHandle) idRef[Package] {
+ return idRef[Package]{
+ id: &t.PackageID,
+ ref: &t.Package,
+ }
+}
+
+func affectedPackageHandleVulnerabilityHandleRef(t *AffectedPackageHandle) idRef[VulnerabilityHandle] {
+ return idRef[VulnerabilityHandle]{
+ id: &t.VulnerabilityID,
+ ref: &t.Vulnerability,
+ }
+}
+
+// fillAffectedCPEHandles lazy loads all properties on the list of AffectedCPEHandles
+func fillAffectedCPEHandles(reader Reader, handles []*AffectedCPEHandle) error {
+ return errors.Join(
+ reader.attachBlobValue(toBlobables(handles)...),
+ fillRefs(reader, handles, affectedCPEHandleCpeRef, cpeHandleID),
+ fillVulnerabilityHandles(reader, handles, affectedCPEHandleVulnerabilityHandleRef),
+ )
+}
+
+func affectedCPEHandleCpeRef(t *AffectedCPEHandle) idRef[Cpe] {
+ return idRef[Cpe]{
+ id: &t.CpeID,
+ ref: &t.CPE,
+ }
+}
+
+func affectedCPEHandleVulnerabilityHandleRef(t *AffectedCPEHandle) idRef[VulnerabilityHandle] {
+ return idRef[VulnerabilityHandle]{
+ id: &t.VulnerabilityID,
+ ref: &t.Vulnerability,
+ }
+}
+
+// fillVulnerabilityHandles lazy loads vulnerability handle properties
+func fillVulnerabilityHandles[T any](reader Reader, handles []*T, vulnHandleRef refProvider[T, VulnerabilityHandle]) error {
+ // fill vulnerabilities
+ if err := fillRefs(reader, handles, vulnHandleRef, vulnerabilityHandleID); err != nil {
+ return err
+ }
+ var providerRefs []ref[string, Provider]
+ vulnHandles := make([]*VulnerabilityHandle, len(handles))
+ for i := range handles {
+ vulnHandles[i] = *vulnHandleRef(handles[i]).ref
+ providerRefs = append(providerRefs, ref[string, Provider]{
+ id: &vulnHandles[i].ProviderID,
+ ref: &vulnHandles[i].Provider,
+ })
+ }
+ // then get references to them to fill the properties
+ return errors.Join(
+ reader.attachBlobValue(toBlobables(vulnHandles)...),
+ reader.fillProviders(providerRefs),
+ )
+}
+
+func vulnerabilityHandleID(h *VulnerabilityHandle) ID {
+ return h.ID
+}
+
+func cpeHandleID(h *Cpe) ID {
+ return h.ID
+}
+
+func operatingSystemID(h *OperatingSystem) ID {
+ return h.ID
+}
+
+func packageID(h *Package) ID {
+ return h.ID
+}
+
+func toBlobables[T blobable](handles []T) []blobable {
+ out := make([]blobable, len(handles))
+ for i := range handles {
+ out[i] = handles[i]
+ }
+ return out
+}
diff --git a/grype/db/v6/import_metadata.go b/grype/db/v6/import_metadata.go
new file mode 100644
index 00000000000..b18e8fa22c0
--- /dev/null
+++ b/grype/db/v6/import_metadata.go
@@ -0,0 +1,97 @@
+package v6
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/OneOfOne/xxhash"
+ "github.com/spf13/afero"
+
+ "github.com/anchore/grype/internal/file"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+const ImportMetadataFileName = "import.json"
+
+type ImportMetadata struct {
+ Digest string `json:"digest"`
+ Source string `json:"source,omitempty"`
+ ClientVersion string `json:"client_version"`
+}
+
+func ReadImportMetadata(fs afero.Fs, dir string) (*ImportMetadata, error) {
+ checksumsFilePath := filepath.Join(dir, ImportMetadataFileName)
+
+ if _, err := fs.Stat(checksumsFilePath); os.IsNotExist(err) {
+ return nil, fmt.Errorf("no import metadata file at: %v", checksumsFilePath)
+ }
+
+ content, err := afero.ReadFile(fs, checksumsFilePath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read import metadata file: %w", err)
+ }
+
+ if len(content) == 0 {
+ return nil, fmt.Errorf("no import metadata found at: %v", checksumsFilePath)
+ }
+
+ var doc ImportMetadata
+ if err := json.Unmarshal(content, &doc); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal import metadata: %w", err)
+ }
+
+ if !strings.HasPrefix(doc.Digest, "xxh64:") {
+ return nil, fmt.Errorf("import metadata digest is not in the expected format")
+ }
+
+ return &doc, nil
+}
+
+func CalculateDBDigest(fs afero.Fs, dbFilePath string) (string, error) {
+ digest, err := file.HashFile(fs, dbFilePath, xxhash.New64())
+ if err != nil {
+ return "", fmt.Errorf("failed to digest DB file: %w", err)
+ }
+ return fmt.Sprintf("xxh64:%s", digest), nil
+}
+
+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 {
+ return nil, fmt.Errorf("failed to create import metadata file: %w", err)
+ }
+ defer f.Close()
+
+ checksums, err := CalculateDBDigest(fs, filepath.Join(dbDir, VulnerabilityDBFileName))
+ if err != nil {
+ return nil, fmt.Errorf("failed to calculate checksum for DB file: %w", err)
+ }
+
+ return writeImportMetadata(f, checksums, source)
+}
+
+func writeImportMetadata(writer io.Writer, checksums, source string) (*ImportMetadata, error) {
+ if checksums == "" {
+ return nil, fmt.Errorf("checksum is required")
+ }
+
+ if !strings.HasPrefix(checksums, "xxh64:") {
+ return nil, fmt.Errorf("checksum missing algorithm prefix")
+ }
+
+ enc := json.NewEncoder(writer)
+ enc.SetIndent("", " ")
+
+ doc := ImportMetadata{
+ Digest: checksums,
+ Source: source,
+ ClientVersion: schemaver.New(ModelVersion, Revision, Addition).String(),
+ }
+
+ return &doc, enc.Encode(doc)
+}
diff --git a/grype/db/v6/import_metadata_test.go b/grype/db/v6/import_metadata_test.go
new file mode 100644
index 00000000000..5b1dd13a98e
--- /dev/null
+++ b/grype/db/v6/import_metadata_test.go
@@ -0,0 +1,171 @@
+package v6
+
+import (
+ "bytes"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+func TestReadImportMetadata(t *testing.T) {
+ tests := []struct {
+ name string
+ fileContent string
+ emptyFile bool
+ expectedErr string
+ expectedResult *ImportMetadata
+ }{
+ {
+ name: "file does not exist",
+ fileContent: "",
+ expectedErr: "no import metadata",
+ },
+ {
+ name: "empty file",
+ emptyFile: true,
+ expectedErr: "no import metadata",
+ },
+ {
+ name: "invalid json",
+ fileContent: "invalid json",
+ expectedErr: "failed to unmarshal import metadata",
+ },
+ {
+ name: "missing checksum prefix",
+ fileContent: `{"digest": "invalid", "client_version": "1.0.0"}`,
+ expectedErr: "import metadata digest is not in the expected format",
+ },
+ {
+ name: "valid metadata",
+ 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",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ dir := t.TempDir()
+ filePath := filepath.Join(dir, ImportMetadataFileName)
+
+ if tt.fileContent != "" {
+ err := os.WriteFile(filePath, []byte(tt.fileContent), 0644)
+ require.NoError(t, err)
+ } else if tt.emptyFile {
+ _, err := os.Create(filePath)
+ require.NoError(t, err)
+ }
+
+ result, err := ReadImportMetadata(afero.NewOsFs(), dir)
+
+ if tt.expectedErr != "" {
+ require.ErrorContains(t, err, tt.expectedErr)
+ require.Nil(t, result)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tt.expectedResult, result)
+ }
+ })
+ }
+}
+
+func TestWriteImportMetadata(t *testing.T) {
+ cases := []struct {
+ name string
+ checksum string
+ expectedVersion string
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "valid checksum",
+ checksum: "xxh64:testdigest",
+ expectedVersion: schemaver.New(ModelVersion, Revision, Addition).String(),
+ wantErr: require.NoError,
+ },
+ {
+ name: "empty checksum",
+ checksum: "",
+ wantErr: require.Error,
+ },
+ {
+ name: "missing prefix",
+ checksum: "testdigest",
+ wantErr: require.Error,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ src := "source!"
+ claim, err := writeImportMetadata(&buf, tc.checksum, src)
+ tc.wantErr(t, err)
+
+ if err == nil {
+ result := buf.String()
+
+ var doc ImportMetadata
+ err := json.Unmarshal([]byte(result), &doc)
+ require.NoError(t, err)
+
+ assert.Equal(t, tc.checksum, doc.Digest)
+ 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)
+ }
+ })
+ }
+}
+
+func TestCalculateDBDigest(t *testing.T) {
+ tests := []struct {
+ name string
+ fileContent string
+ expectedErr string
+ expectedDigest string
+ }{
+ {
+ name: "file does not exist",
+ fileContent: "",
+ expectedErr: "failed to digest DB file",
+ },
+ {
+ name: "valid file",
+ fileContent: "testcontent",
+ expectedDigest: "xxh64:d37ed71e4fee2ebd",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ dir := t.TempDir()
+ filePath := filepath.Join(dir, VulnerabilityDBFileName)
+
+ if tt.fileContent != "" {
+ err := os.WriteFile(filePath, []byte(tt.fileContent), 0644)
+ require.NoError(t, err)
+ }
+
+ digest, err := CalculateDBDigest(afero.NewOsFs(), filePath)
+
+ if tt.expectedErr != "" {
+ require.ErrorContains(t, err, tt.expectedErr)
+ require.Empty(t, digest)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tt.expectedDigest, digest)
+ }
+ })
+ }
+}
diff --git a/grype/db/v6/installation/curator.go b/grype/db/v6/installation/curator.go
new file mode 100644
index 00000000000..1c8c853c2aa
--- /dev/null
+++ b/grype/db/v6/installation/curator.go
@@ -0,0 +1,659 @@
+package installation
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/adrg/xdg"
+ "github.com/hako/durafmt"
+ "github.com/spf13/afero"
+ "github.com/wagoodman/go-partybus"
+ "github.com/wagoodman/go-progress"
+
+ "github.com/anchore/archiver/v3"
+ "github.com/anchore/clio"
+ 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"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+const lastUpdateCheckFileName = "last_update_check"
+
+type monitor struct {
+ *progress.AtomicStage
+ downloadProgress completionMonitor
+ importProgress completionMonitor
+ hydrateProgress completionMonitor
+}
+
+type Config struct {
+ DBRootDir string
+ Debug bool
+
+ // validations
+ ValidateAge bool
+ ValidateChecksum bool
+ MaxAllowedBuiltAge time.Duration
+ UpdateCheckMaxFrequency time.Duration
+}
+
+func DefaultConfig(id clio.Identification) Config {
+ return Config{
+ DBRootDir: filepath.Join(xdg.CacheHome, id.Name, "db"),
+ ValidateAge: true,
+ ValidateChecksum: true,
+ MaxAllowedBuiltAge: time.Hour * 24 * 5, // 5 days
+ UpdateCheckMaxFrequency: 2 * time.Hour, // 2 hours
+ }
+}
+
+func (c Config) DBFilePath() string {
+ return filepath.Join(c.DBDirectoryPath(), db.VulnerabilityDBFileName)
+}
+
+func (c Config) DBDirectoryPath() string {
+ return filepath.Join(c.DBRootDir, strconv.Itoa(db.ModelVersion))
+}
+
+type curator struct {
+ fs afero.Fs
+ client distribution.Client
+ config Config
+ hydrator func(string) error
+}
+
+func NewCurator(cfg Config, downloader distribution.Client) (db.Curator, error) {
+ return curator{
+ fs: afero.NewOsFs(),
+ client: downloader,
+ config: cfg,
+ hydrator: db.Hydrater(),
+ }, nil
+}
+
+func (c curator) Reader() (db.Reader, error) {
+ s, err := db.NewReader(
+ db.Config{
+ DBDirPath: c.config.DBDirectoryPath(),
+ Debug: c.config.Debug,
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ m, err := s.GetDBMetadata()
+ if err != nil {
+ return nil, fmt.Errorf("unable to get vulnerability store metadata: %w", err)
+ }
+
+ var currentDBSchemaVersion *schemaver.SchemaVer
+ if m != nil {
+ v := schemaver.New(m.Model, m.Revision, m.Addition)
+ currentDBSchemaVersion = &v
+ }
+
+ doRehydrate, err := isRehydrationNeeded(c.fs, c.config.DBDirectoryPath(), currentDBSchemaVersion, schemaver.New(db.ModelVersion, db.Revision, db.Addition))
+ if err != nil {
+ return nil, err
+ }
+ if doRehydrate {
+ if err = s.Close(); err != nil {
+ // DB connection may be in an inconsistent state -- we cannot continue
+ return nil, fmt.Errorf("unable to close reader before rehydration: %w", err)
+ }
+ mon := newMonitor()
+
+ 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(), source, mon); err != nil {
+ log.WithFields("error", err).Warn("unable to rehydrate DB")
+ }
+ mon.Set("rehydrated")
+ mon.SetCompleted()
+
+ s, err = db.NewReader(
+ db.Config{
+ DBDirPath: c.config.DBDirectoryPath(),
+ Debug: c.config.Debug,
+ },
+ )
+ if err != nil {
+ return nil, fmt.Errorf("unable to create new reader after rehydration: %w", err)
+ }
+ }
+
+ return s, nil
+}
+
+func (c curator) Status() vulnerability.ProviderStatus {
+ dbFile := c.config.DBFilePath()
+ d, validateErr := db.ReadDescription(dbFile)
+ if validateErr != nil {
+ return vulnerability.ProviderStatus{
+ Path: dbFile,
+ Error: validateErr,
+ }
+ }
+ if d == nil {
+ return vulnerability.ProviderStatus{
+ Path: dbFile,
+ Error: fmt.Errorf("database not found at %q", dbFile),
+ }
+ }
+
+ validateErr = c.validateAge(d)
+ _, checksumErr := c.validateIntegrity(d)
+ if checksumErr != nil && c.config.ValidateChecksum {
+ if validateErr != nil {
+ validateErr = errors.Join(validateErr, checksumErr)
+ } else {
+ validateErr = checksumErr
+ }
+ }
+
+ 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,
+ Error: validateErr,
+ }
+}
+
+// Delete removes the DB and metadata file for this specific schema.
+func (c curator) Delete() error {
+ return c.fs.RemoveAll(c.config.DBDirectoryPath())
+}
+
+// Update the existing DB, returning an indication if any action was taken.
+func (c curator) Update() (bool, error) {
+ current, err := db.ReadDescription(c.config.DBFilePath())
+ if err != nil {
+ // we should not warn if the DB does not exist, as this is a common first-run case... but other cases we
+ // may care about, so warn in those cases.
+ if !errors.Is(err, db.ErrDBDoesNotExist) {
+ log.WithFields("error", err).Warn("unable to read current database metadata; continuing with update")
+ }
+ // downstream any non-existent DB should always be replaced with any best-candidate found
+ current = nil
+ } else {
+ err = c.validateAge(current)
+ if err != nil {
+ // even if we are not allowed to check for an update, we should still attempt to update the DB if it is invalid
+ log.WithFields("error", err).Warn("current database is invalid")
+ current = nil
+ }
+ }
+
+ if current != nil && !c.isUpdateCheckAllowed() {
+ // we should not notify the user of an update check if the current configuration and state
+ // indicates we are in a low-pass filter mode and the check frequency is too high.
+ // this should appear to the user as if we never attempted to check for an update at all.
+ return false, nil
+ }
+
+ update, err := c.update(current)
+ if err != nil {
+ return false, err
+ }
+
+ if update == nil {
+ return false, nil
+ }
+
+ if current != nil {
+ log.WithFields(
+ "from", current.Built.String(),
+ "to", update.Description.Built.String(),
+ "version", update.Description.SchemaVersion,
+ ).Info("updated vulnerability DB")
+ return true, nil
+ }
+
+ log.WithFields(
+ "version", update.Description.SchemaVersion,
+ "built", update.Description.Built.String(),
+ ).Info("installed new vulnerability DB")
+ return true, nil
+}
+
+func (c curator) isUpdateCheckAllowed() bool {
+ if c.config.UpdateCheckMaxFrequency == 0 {
+ log.Trace("no max-frequency set for update check")
+ return true
+ }
+
+ elapsed, err := c.durationSinceUpdateCheck()
+ if err != nil {
+ // we had an IO error (or similar) trying to read or parse the file, we should not block the update check.
+ log.WithFields("error", err).Trace("unable to determine if update check is allowed")
+ return true
+ }
+ if elapsed == nil {
+ // there was no last check (this is a first run case), we should not block the update check.
+ return true
+ }
+
+ return *elapsed > c.config.UpdateCheckMaxFrequency
+}
+
+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)
+ if checkErr != nil {
+ // we want to continue even if we can't check for an update
+ log.Warnf("unable to check for vulnerability database update")
+ log.WithFields("error", checkErr).Debug("check for vulnerability update failed")
+ }
+
+ if update == nil {
+ if checkErr == nil {
+ // there was no update (or any issue while checking for an update)
+ c.setLastSuccessfulUpdateCheck()
+ }
+
+ mon.Set("no update available")
+ return nil, checkErr
+ }
+
+ log.Info("downloading new vulnerability DB")
+ mon.Set("downloading")
+ url, err := c.client.ResolveArchiveURL(*update)
+ if err != nil {
+ return nil, fmt.Errorf("unable to resolve vulnerability DB URL: %w", err)
+ }
+
+ // Ensure parent of DBRootDir exists for the download client to create a temp dir within DBRootDir
+ // This might be redundant if DBRootDir must already exist, but good for safety.
+ if err := os.MkdirAll(c.config.DBRootDir, 0o700); err != nil {
+ return nil, fmt.Errorf("unable to create db root dir %s for download: %w", c.config.DBRootDir, err)
+ }
+
+ dest, err := c.client.Download(url, 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, url, mon); err != nil {
+ log.Warnf("Failed to activate downloaded database from %s, attempting cleanup of temporary download directory.", dest)
+ removeAllOrLog(c.fs, dest)
+ return nil, fmt.Errorf("unable to activate new vulnerability database: %w", err)
+ }
+
+ mon.Set("updated")
+
+ // only set the last successful update check if the update was successful
+ c.setLastSuccessfulUpdateCheck()
+
+ return update, nil
+}
+
+func isRehydrationNeeded(fs afero.Fs, dirPath string, currentDBVersion *schemaver.SchemaVer, currentClientVersion schemaver.SchemaVer) (bool, error) {
+ if currentDBVersion == nil {
+ // there is no DB to rehydrate
+ return false, nil
+ }
+
+ importMetadata, err := db.ReadImportMetadata(fs, dirPath)
+ if err != nil {
+ return false, fmt.Errorf("unable to read import metadata: %w", err)
+ }
+
+ clientHydrationVersion, err := schemaver.Parse(importMetadata.ClientVersion)
+ if err != nil {
+ return false, fmt.Errorf("unable to parse client version from import metadata: %w", err)
+ }
+
+ hydratedWithOldClient := clientHydrationVersion.LessThan(*currentDBVersion)
+ haveNewerClient := clientHydrationVersion.LessThan(currentClientVersion)
+ doRehydrate := hydratedWithOldClient && haveNewerClient
+
+ msg := "DB rehydration not needed"
+ if doRehydrate {
+ msg = "DB rehydration needed"
+ }
+
+ log.WithFields("clientHydrationVersion", clientHydrationVersion, "currentDBVersion", currentDBVersion, "currentClientVersion", currentClientVersion).Trace(msg)
+
+ if doRehydrate {
+ // 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.
+ return true, nil
+ }
+
+ return false, nil
+}
+
+func (c curator) durationSinceUpdateCheck() (*time.Duration, error) {
+ // open `$dbDir/last_update_check` file and read the timestamp and do now() - timestamp
+
+ filePath := filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)
+
+ if _, err := c.fs.Stat(filePath); os.IsNotExist(err) {
+ log.Trace("first-run of DB update")
+ return nil, nil
+ }
+
+ fh, err := c.fs.OpenFile(filePath, os.O_RDONLY, 0)
+ if err != nil {
+ return nil, fmt.Errorf("unable to read last update check timestamp: %w", err)
+ }
+
+ defer log.CloseAndLogError(fh, filePath)
+
+ // read and parse rfc3339 timestamp
+ var lastCheckStr string
+ _, err = fmt.Fscanf(fh, "%s", &lastCheckStr)
+ if err != nil {
+ return nil, fmt.Errorf("unable to read last update check timestamp: %w", err)
+ }
+
+ lastCheck, err := time.Parse(time.RFC3339, lastCheckStr)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse last update check timestamp: %w", err)
+ }
+
+ if lastCheck.IsZero() {
+ return nil, fmt.Errorf("empty update check timestamp")
+ }
+
+ elapsed := time.Since(lastCheck)
+ return &elapsed, nil
+}
+
+func (c curator) setLastSuccessfulUpdateCheck() {
+ // note: we should always assume the DB dir actually exists, otherwise let this operation fail (since having a DB
+ // is a prerequisite for a successful update).
+
+ filePath := filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)
+ fh, err := c.fs.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
+ if err != nil {
+ log.WithFields("error", err).Trace("unable to write last update check timestamp")
+ return
+ }
+
+ defer log.CloseAndLogError(fh, filePath)
+
+ _, _ = fmt.Fprintf(fh, "%s", time.Now().UTC().Format(time.RFC3339))
+}
+
+// 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("preparing")
+ defer mon.SetCompleted()
+
+ if err := os.MkdirAll(c.config.DBRootDir, 0o700); err != nil {
+ return fmt.Errorf("unable to create db root dir: %w", err)
+ }
+
+ var tempDir, url string
+ if isURL(reference) {
+ log.Info("downloading new vulnerability DB")
+ mon.Set("downloading")
+ var err error
+
+ tempDir, err = c.client.Download(reference, c.config.DBRootDir, mon.downloadProgress.Manual)
+ if err != nil {
+ return fmt.Errorf("unable to update vulnerability database: %w", err)
+ }
+
+ url = reference
+ } else {
+ // 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 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()
+
+ if err := c.activate(tempDir, url, mon); err != nil {
+ removeAllOrLog(c.fs, tempDir)
+ return err
+ }
+
+ mon.Set("imported")
+
+ 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, url string, mon monitor) error {
+ defer mon.SetCompleted()
+
+ 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, from string, mon monitor) error {
+ if c.hydrator != nil {
+ mon.Set("hydrating")
+ if err := c.hydrator(dbDirPath); err != nil {
+ return err
+ }
+ }
+ mon.hydrateProgress.SetCompleted()
+
+ mon.Set("hashing")
+
+ doc, err := db.WriteImportMetadata(c.fs, dbDirPath, from)
+ if err != nil {
+ return fmt.Errorf("failed to write checksums file: %w", err)
+ }
+
+ log.WithFields("digest", doc.Digest).Trace("captured DB digest")
+
+ return nil
+}
+
+// replaceDB swaps over to using the given path.
+func (c curator) replaceDB(dbDirPath string) error {
+ dbDir := c.config.DBDirectoryPath()
+ _, err := c.fs.Stat(dbDir)
+ if !os.IsNotExist(err) {
+ // remove any previous databases
+ err = c.Delete()
+ if err != nil {
+ return fmt.Errorf("failed to purge existing database: %w", err)
+ }
+ }
+
+ // ensure parent db directory exists
+ if err = c.fs.MkdirAll(filepath.Dir(dbDir), 0o700); err != nil {
+ return fmt.Errorf("unable to create db parent directory: %w", err)
+ }
+
+ // activate the new db cache by moving the temp dir to final location
+ // the rename should be safe because the temp dir is under GRYPE_DB_CACHE_DIR
+ // and so on the same filesystem as the final location
+ err = c.fs.Rename(dbDirPath, dbDir)
+ if err != nil {
+ err = fmt.Errorf("failed to move database directory to activate: %w", err)
+ }
+ log.WithFields("from", dbDirPath, "to", dbDir, "error", err).Debug("moved database directory to activate")
+ return err
+}
+
+// validateIntegrity checks that the disk checksum still matches the db payload
+func (c curator) validateIntegrity(description *db.Description) (string, error) {
+ dbFilePath := c.config.DBFilePath()
+
+ // check that the disk checksum still matches the db payload
+ if description == nil {
+ return "", fmt.Errorf("database not found: %s", dbFilePath)
+ }
+
+ if description.SchemaVersion.Model != db.ModelVersion {
+ return "", fmt.Errorf("unsupported database version: have=%d want=%d", description.SchemaVersion.Model, db.ModelVersion)
+ }
+
+ if _, err := c.fs.Stat(dbFilePath); err != nil {
+ if os.IsNotExist(err) {
+ return "", fmt.Errorf("database does not exist: %s", dbFilePath)
+ }
+ return "", fmt.Errorf("failed to access database file: %w", err)
+ }
+
+ importMetadata, err := db.ReadImportMetadata(c.fs, filepath.Dir(dbFilePath))
+ if err != nil {
+ return "", err
+ }
+
+ valid, actualHash, err := file.ValidateByHash(c.fs, dbFilePath, importMetadata.Digest)
+ if err != nil {
+ return actualHash, err
+ }
+ if !valid {
+ return actualHash, fmt.Errorf("bad db checksum (%s): %q vs %q", dbFilePath, importMetadata.Digest, actualHash)
+ }
+
+ return actualHash, nil
+}
+
+// validateAge ensures the vulnerability database has not passed
+// the max allowed age, calculated from the time it was built until now.
+func (c curator) validateAge(m *db.Description) error {
+ if m == nil {
+ return fmt.Errorf("no metadata to validate")
+ }
+
+ if !c.config.ValidateAge {
+ return nil
+ }
+
+ // built time is defined in UTC,
+ // we should compare it against UTC
+ now := time.Now().UTC()
+
+ age := now.Sub(m.Built.Time)
+ if age > c.config.MaxAllowedBuiltAge {
+ return fmt.Errorf("the vulnerability database was built %s ago (max allowed age is %s)", durafmt.ParseShort(age), durafmt.ParseShort(c.config.MaxAllowedBuiltAge))
+ }
+
+ return nil
+}
+
+func removeAllOrLog(fs afero.Fs, dir string) {
+ if err := fs.RemoveAll(dir); err != nil {
+ log.WithFields("error", err).Warnf("failed to remove path %q", dir)
+ }
+}
+
+func newMonitor() monitor {
+ // let consumers know of a monitorable event (download + import stages)
+ importProgress := progress.NewManual(1)
+ stage := progress.NewAtomicStage("")
+ downloadProgress := progress.NewManual(1)
+ hydrateProgress := progress.NewManual(1)
+ aggregateProgress := progress.NewAggregator(progress.DefaultStrategy, downloadProgress, hydrateProgress, importProgress)
+
+ bus.Publish(partybus.Event{
+ Type: event.UpdateVulnerabilityDatabase,
+ Value: progress.StagedProgressable(&struct {
+ progress.Stager
+ progress.Progressable
+ }{
+ Stager: progress.Stager(stage),
+ Progressable: progress.Progressable(aggregateProgress),
+ }),
+ })
+
+ return monitor{
+ AtomicStage: stage,
+ downloadProgress: completionMonitor{downloadProgress},
+ importProgress: completionMonitor{importProgress},
+ hydrateProgress: completionMonitor{hydrateProgress},
+ }
+}
+
+func (m monitor) SetCompleted() {
+ m.downloadProgress.SetCompleted()
+ m.importProgress.SetCompleted()
+ m.hydrateProgress.SetCompleted()
+}
+
+// completionMonitor is a progressable that, when SetComplete() is called, will set the progress to the total size
+type completionMonitor struct {
+ *progress.Manual
+}
+
+func (m completionMonitor) SetCompleted() {
+ m.Set(m.Size())
+ m.Manual.SetCompleted()
+}
diff --git a/grype/db/v6/installation/curator_test.go b/grype/db/v6/installation/curator_test.go
new file mode 100644
index 00000000000..9d60d34582f
--- /dev/null
+++ b/grype/db/v6/installation/curator_test.go
@@ -0,0 +1,930 @@
+package installation
+
+import (
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+ "github.com/wagoodman/go-progress"
+
+ "github.com/anchore/clio"
+ db "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+type mockClient struct {
+ mock.Mock
+}
+
+func (m *mockClient) IsUpdateAvailable(current *db.Description) (*distribution.Archive, error) {
+ args := m.Called(current)
+
+ err := args.Error(1)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return args.Get(0).(*distribution.Archive), nil
+}
+
+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)
+}
+
+func (m *mockClient) Latest() (*distribution.LatestDocument, error) {
+ args := m.Called()
+ return args.Get(0).(*distribution.LatestDocument), args.Error(1)
+}
+
+func newTestCurator(t *testing.T) curator {
+ tempDir := t.TempDir()
+ cfg := testConfig()
+ cfg.DBRootDir = tempDir
+
+ ci, err := NewCurator(cfg, new(mockClient))
+ require.NoError(t, err)
+
+ c := ci.(curator)
+ return c
+}
+
+type setupConfig struct {
+ workingUpdate bool
+}
+
+type setupOption func(*setupConfig)
+
+func withWorkingUpdateIntegrations() setupOption {
+ return func(c *setupConfig) {
+ c.workingUpdate = true
+ }
+}
+
+func setupCuratorForUpdate(t *testing.T, opts ...setupOption) curator {
+ cfg := setupConfig{}
+
+ for _, o := range opts {
+ o(&cfg)
+ }
+
+ c := newTestCurator(t)
+
+ dbDir := c.config.DBDirectoryPath()
+ stageConfig := Config{DBRootDir: filepath.Join(c.config.DBRootDir, "staged")}
+ stageDir := stageConfig.DBDirectoryPath()
+
+ // populate metadata into the downloaded dir
+ oldDescription := db.Description{
+ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+ Built: db.Time{Time: time.Now().Add(-48 * time.Hour)},
+ }
+ writeTestDB(t, c.fs, dbDir)
+
+ newDescription := oldDescription
+ newDescription.Built = db.Time{Time: time.Now()}
+
+ writeTestDB(t, c.fs, stageDir)
+
+ writeTestDescriptionToDB(t, dbDir, oldDescription)
+ writeTestDescriptionToDB(t, stageDir, newDescription)
+
+ if cfg.workingUpdate {
+ mc := c.client.(*mockClient)
+
+ // ensure the update "works"
+ mc.On("IsUpdateAvailable", mock.Anything).Return(&distribution.Archive{}, nil)
+ mc.On("Download", mock.Anything, mock.Anything, mock.Anything).Return(stageDir, nil)
+ }
+
+ return c
+}
+
+func writeTestDescriptionToDB(t *testing.T, dir string, desc db.Description) string {
+ c := db.Config{DBDirPath: dir}
+ d, err := db.NewLowLevelDB(c.DBFilePath(), false, true, true)
+ require.NoError(t, err)
+
+ if err := d.Where("true").Delete(&db.DBMetadata{}).Error; err != nil {
+ t.Fatalf("failed to delete existing DB metadata record: %v", err)
+ }
+
+ require.NotEmpty(t, desc.SchemaVersion.Model)
+ require.NotEmpty(t, desc.SchemaVersion.String())
+
+ ts := time.Now().UTC()
+ instance := &db.DBMetadata{
+ BuildTimestamp: &ts,
+ Model: desc.SchemaVersion.Model,
+ Revision: desc.SchemaVersion.Revision,
+ Addition: desc.SchemaVersion.Revision,
+ }
+
+ require.NoError(t, d.Create(instance).Error)
+
+ require.NoError(t, d.Exec("VACUUM").Error)
+
+ digest, err := db.CalculateDBDigest(afero.NewOsFs(), c.DBFilePath())
+ require.NoError(t, err)
+
+ writeTestImportMetadata(t, afero.NewOsFs(), dir, digest)
+
+ return digest
+}
+
+func writeTestImportMetadata(t *testing.T, fs afero.Fs, dir string, checksums string) {
+ writeTestImportMetadataWithCustomVersion(t, fs, dir, checksums, schemaver.New(db.ModelVersion, db.Revision, db.Addition).String())
+}
+
+func writeTestImportMetadataWithCustomVersion(t *testing.T, fs afero.Fs, dir string, checksums string, ver string) {
+ require.NoError(t, fs.MkdirAll(dir, 0755))
+
+ metadataFilePath := filepath.Join(dir, db.ImportMetadataFileName)
+
+ writer, err := afero.NewOsFs().Create(metadataFilePath)
+ require.NoError(t, err)
+ defer func() { _ = writer.Close() }()
+ enc := json.NewEncoder(writer)
+ enc.SetIndent("", " ")
+
+ doc := db.ImportMetadata{
+ Digest: checksums,
+ ClientVersion: ver,
+ }
+
+ require.NoError(t, enc.Encode(doc))
+}
+
+func writeTestDB(t *testing.T, fs afero.Fs, dir string) string {
+ require.NoError(t, fs.MkdirAll(dir, 0755))
+
+ rw, err := db.NewWriter(db.Config{
+ DBDirPath: dir,
+ })
+ require.NoError(t, err)
+
+ require.NoError(t, rw.SetDBMetadata())
+ require.NoError(t, rw.Close())
+
+ doc, err := db.WriteImportMetadata(fs, dir, "source")
+ require.NoError(t, err)
+ require.NotNil(t, doc)
+
+ return doc.Digest
+}
+
+func TestCurator_Update(t *testing.T) {
+
+ t.Run("happy path: successful update", func(t *testing.T) {
+ c := setupCuratorForUpdate(t, withWorkingUpdateIntegrations())
+ mc := c.client.(*mockClient)
+ // nop hydrator, assert error if NOT called
+ hydrateCalled := false
+ c.hydrator = func(string) error {
+ hydrateCalled = true
+ return nil
+ }
+
+ updated, err := c.Update()
+
+ require.NoError(t, err)
+ require.True(t, updated)
+ require.FileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))
+
+ mc.AssertExpectations(t)
+ assert.True(t, hydrateCalled, "expected hydrator to be called")
+ })
+
+ t.Run("error checking for updates", func(t *testing.T) {
+ c := setupCuratorForUpdate(t)
+ mc := c.client.(*mockClient)
+
+ mc.On("IsUpdateAvailable", mock.Anything).Return(nil, errors.New("check failed"))
+
+ updated, err := c.Update()
+
+ require.Error(t, err)
+ require.False(t, updated)
+ require.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))
+
+ mc.AssertExpectations(t)
+ })
+
+ t.Run("error during download", func(t *testing.T) {
+ c := setupCuratorForUpdate(t)
+ mc := c.client.(*mockClient)
+
+ mc.On("IsUpdateAvailable", mock.Anything).Return(&distribution.Archive{}, nil)
+ mc.On("Download", mock.Anything, mock.Anything, mock.Anything).Return("", errors.New("download failed"))
+
+ updated, err := c.Update()
+
+ require.ErrorContains(t, err, "download failed")
+ require.False(t, updated)
+ require.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))
+
+ mc.AssertExpectations(t)
+ })
+
+ t.Run("error during activation: cannot move dir", func(t *testing.T) {
+ c := setupCuratorForUpdate(t, withWorkingUpdateIntegrations())
+ mc := c.client.(*mockClient)
+ // nop hydrator
+ c.hydrator = nil
+
+ // simulate not being able to move the staged dir to the db dir
+ c.fs = afero.NewReadOnlyFs(c.fs)
+
+ updated, err := c.Update()
+
+ require.ErrorContains(t, err, "operation not permitted")
+ require.False(t, updated)
+ require.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))
+
+ mc.AssertExpectations(t)
+ })
+}
+
+func TestCurator_IsUpdateCheckAllowed(t *testing.T) {
+
+ newCurator := func(t *testing.T) curator {
+ tempDir := t.TempDir()
+
+ cfg := testConfig()
+ cfg.UpdateCheckMaxFrequency = 10 * time.Minute
+ cfg.DBRootDir = tempDir
+
+ ci, err := NewCurator(cfg, nil)
+ require.NoError(t, err)
+
+ c := ci.(curator)
+ return c
+ }
+
+ writeLastCheckContents := func(t *testing.T, cfg Config, contents string) {
+ require.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755))
+ p := filepath.Join(cfg.DBDirectoryPath(), lastUpdateCheckFileName)
+ err := os.WriteFile(p, []byte(contents), 0644)
+ require.NoError(t, err)
+ }
+
+ writeLastCheckTime := func(t *testing.T, cfg Config, lastCheckTime time.Time) {
+ writeLastCheckContents(t, cfg, lastCheckTime.Format(time.RFC3339))
+ }
+
+ t.Run("first run check (no last check file)", func(t *testing.T) {
+ c := newCurator(t)
+ require.True(t, c.isUpdateCheckAllowed())
+ })
+
+ t.Run("check not allowed due to frequency", func(t *testing.T) {
+ c := newCurator(t)
+ writeLastCheckTime(t, c.config, time.Now().Add(-5*time.Minute))
+
+ require.False(t, c.isUpdateCheckAllowed())
+ })
+
+ t.Run("check allowed after the frequency period", func(t *testing.T) {
+ c := newCurator(t)
+ writeLastCheckTime(t, c.config, time.Now().Add(-20*time.Minute))
+
+ require.True(t, c.isUpdateCheckAllowed())
+ })
+
+ t.Run("error reading last check file", func(t *testing.T) {
+ c := newCurator(t)
+
+ // simulate a situation where the last check file exists but is corrupted
+ writeLastCheckContents(t, c.config, "invalid timestamp")
+
+ allowed := c.isUpdateCheckAllowed()
+ require.True(t, allowed) // should return true since an error is encountered
+ })
+
+}
+
+func TestCurator_DurationSinceUpdateCheck(t *testing.T) {
+ newCurator := func(t *testing.T) curator {
+ tempDir := t.TempDir()
+
+ cfg := testConfig()
+ cfg.DBRootDir = tempDir
+
+ ci, err := NewCurator(cfg, nil)
+ require.NoError(t, err)
+
+ c := ci.(curator)
+ return c
+ }
+
+ writeLastCheckContents := func(t *testing.T, cfg Config, contents string) {
+ require.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755))
+ p := filepath.Join(cfg.DBDirectoryPath(), lastUpdateCheckFileName)
+ err := os.WriteFile(p, []byte(contents), 0644)
+ require.NoError(t, err)
+ }
+
+ t.Run("no last check file", func(t *testing.T) {
+ c := newCurator(t)
+ elapsed, err := c.durationSinceUpdateCheck()
+ require.NoError(t, err)
+ require.Nil(t, elapsed) // should be nil since no file exists
+ })
+
+ t.Run("valid last check file", func(t *testing.T) {
+ c := newCurator(t)
+ writeLastCheckContents(t, c.config, time.Now().Add(-5*time.Minute).Format(time.RFC3339))
+
+ elapsed, err := c.durationSinceUpdateCheck()
+ require.NoError(t, err)
+ require.NotNil(t, elapsed)
+ require.True(t, *elapsed >= 5*time.Minute) // should be at least 5 minutes
+ })
+
+ t.Run("malformed last check file", func(t *testing.T) {
+ c := newCurator(t)
+ writeLastCheckContents(t, c.config, "invalid timestamp")
+
+ _, err := c.durationSinceUpdateCheck()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unable to parse last update check timestamp")
+ })
+}
+
+func TestCurator_SetLastSuccessfulUpdateCheck(t *testing.T) {
+ newCurator := func(t *testing.T) curator {
+ tempDir := t.TempDir()
+
+ cfg := testConfig()
+ cfg.DBRootDir = tempDir
+
+ ci, err := NewCurator(cfg, nil)
+ require.NoError(t, err)
+
+ c := ci.(curator)
+
+ require.NoError(t, c.fs.MkdirAll(c.config.DBDirectoryPath(), 0755))
+
+ return c
+ }
+
+ t.Run("set last successful update check", func(t *testing.T) {
+ c := newCurator(t)
+
+ c.setLastSuccessfulUpdateCheck()
+
+ data, err := afero.ReadFile(c.fs, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))
+ require.NoError(t, err)
+
+ lastCheckTime, err := time.Parse(time.RFC3339, string(data))
+ require.NoError(t, err)
+ require.WithinDuration(t, time.Now().UTC(), lastCheckTime, time.Second)
+ })
+
+ t.Run("error writing last successful update check", func(t *testing.T) {
+ c := newCurator(t)
+
+ // make the file system read-only to simulate a write error
+ readonlyFs := afero.NewReadOnlyFs(c.fs)
+ c.fs = readonlyFs
+
+ c.setLastSuccessfulUpdateCheck()
+
+ require.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))
+ })
+
+ t.Run("ensure last successful update check file is created", func(t *testing.T) {
+ c := newCurator(t)
+
+ c.setLastSuccessfulUpdateCheck()
+
+ require.FileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))
+ })
+}
+
+func TestCurator_validateAge(t *testing.T) {
+ newCurator := func(t *testing.T) curator {
+ tempDir := t.TempDir()
+ cfg := testConfig()
+ cfg.DBRootDir = tempDir
+ cfg.MaxAllowedBuiltAge = 48 * time.Hour // set max age to 48 hours
+
+ ci, err := NewCurator(cfg, new(mockClient))
+ require.NoError(t, err)
+
+ return ci.(curator)
+ }
+
+ hoursAgo := func(h int) db.Time {
+ return db.Time{Time: time.Now().UTC().Add(-time.Duration(h) * time.Hour)}
+ }
+
+ tests := []struct {
+ name string
+ description *db.Description
+ wantErr require.ErrorAssertionFunc
+ modifyConfig func(*Config)
+ }{
+ {
+ name: "valid metadata within age limit",
+ description: &db.Description{
+ Built: hoursAgo(24),
+ },
+ },
+ {
+ name: "stale metadata exactly at age limit",
+ description: &db.Description{
+ Built: hoursAgo(48),
+ },
+ wantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) {
+ require.ErrorContains(t, err, "the vulnerability database was built")
+ },
+ },
+ {
+ name: "stale metadata",
+ description: &db.Description{
+ Built: hoursAgo(50),
+ },
+ wantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) {
+ require.ErrorContains(t, err, "the vulnerability database was built")
+ },
+ },
+ {
+ name: "no metadata",
+ description: nil,
+ wantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) {
+ require.ErrorContains(t, err, "no metadata to validate")
+ },
+ },
+ {
+ name: "age validation disabled",
+ description: &db.Description{
+ Built: hoursAgo(50),
+ },
+ modifyConfig: func(cfg *Config) {
+ cfg.ValidateAge = false
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.wantErr == nil {
+ tt.wantErr = require.NoError
+ }
+
+ c := newCurator(t)
+
+ if tt.modifyConfig != nil {
+ tt.modifyConfig(&c.config)
+ }
+
+ err := c.validateAge(tt.description)
+ tt.wantErr(t, err)
+ })
+ }
+}
+
+func TestCurator_validateIntegrity(t *testing.T) {
+ newCurator := func(t *testing.T) (curator, *db.Description) {
+ tempDir := t.TempDir()
+ cfg := testConfig()
+ cfg.DBRootDir = tempDir
+
+ require.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755))
+
+ sw := setupTestDB(t, cfg.DBDirectoryPath())
+ require.NoError(t, sw.SetDBMetadata())
+ require.NoError(t, sw.Close())
+ s := setupReadOnlyTestDB(t, cfg.DBDirectoryPath())
+
+ // assume that we already have a valid checksum file
+ digest, err := db.CalculateDBDigest(afero.NewOsFs(), cfg.DBFilePath())
+ require.NoError(t, err)
+
+ writeTestImportMetadata(t, afero.NewOsFs(), cfg.DBDirectoryPath(), digest)
+
+ ci, err := NewCurator(cfg, new(mockClient))
+ require.NoError(t, err)
+
+ m, err := s.GetDBMetadata()
+ require.NoError(t, err)
+
+ return ci.(curator), db.DescriptionFromMetadata(m)
+ }
+
+ t.Run("valid metadata with correct checksum", func(t *testing.T) {
+ c, d := newCurator(t)
+
+ digest, err := c.validateIntegrity(d)
+ require.NoError(t, err)
+ require.NotEmpty(t, digest)
+ })
+
+ t.Run("db does not exist", func(t *testing.T) {
+ c, d := newCurator(t)
+
+ require.NoError(t, os.Remove(c.config.DBFilePath()))
+
+ _, err := c.validateIntegrity(d)
+ require.ErrorContains(t, err, "database does not exist")
+ })
+
+ t.Run("import metadata file does not exist", func(t *testing.T) {
+ c, d := newCurator(t)
+ dbDir := c.config.DBDirectoryPath()
+ require.NoError(t, os.Remove(filepath.Join(dbDir, db.ImportMetadataFileName)))
+ _, err := c.validateIntegrity(d)
+ require.ErrorContains(t, err, "no import metadata")
+ })
+
+ t.Run("invalid checksum", func(t *testing.T) {
+ c, d := newCurator(t)
+ dbDir := c.config.DBDirectoryPath()
+
+ writeTestImportMetadata(t, c.fs, dbDir, "xxh64:invalidchecksum")
+
+ _, err := c.validateIntegrity(d)
+ require.ErrorContains(t, err, "bad db checksum")
+ })
+
+ t.Run("unsupported database version", func(t *testing.T) {
+ c, d := newCurator(t)
+
+ d.SchemaVersion = schemaver.New(db.ModelVersion-1, 0, 0)
+
+ _, err := c.validateIntegrity(d)
+ require.ErrorContains(t, err, "unsupported database version")
+ })
+}
+
+func TestReplaceDB(t *testing.T) {
+ cases := []struct {
+ name string
+ config Config
+ expected map[string]string // expected file name to content mapping in the DB dir
+ init func(t *testing.T, dir string, dbDir string) afero.Fs
+ wantErr require.ErrorAssertionFunc
+ verify func(t *testing.T, fs afero.Fs, config Config, expected map[string]string)
+ }{
+ {
+ name: "replace non-existent DB",
+ config: Config{
+ DBRootDir: "/test",
+ },
+ expected: map[string]string{
+ "file.txt": "new content",
+ },
+ init: func(t *testing.T, dir string, dbDir string) afero.Fs {
+ fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir())
+ require.NoError(t, fs.MkdirAll(dir, 0700))
+ require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "file.txt"), []byte("new content"), 0644))
+ return fs
+ },
+ },
+ {
+ name: "replace existing DB",
+ config: Config{
+ DBRootDir: "/test",
+ },
+ expected: map[string]string{
+ "new_file.txt": "new content",
+ },
+ init: func(t *testing.T, dir string, dbDir string) afero.Fs {
+ fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir())
+ require.NoError(t, fs.MkdirAll(dbDir, 0700))
+ require.NoError(t, afero.WriteFile(fs, filepath.Join(dbDir, "old_file.txt"), []byte("old content"), 0644))
+ require.NoError(t, fs.MkdirAll(dir, 0700))
+ require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "new_file.txt"), []byte("new content"), 0644))
+ return fs
+ },
+ },
+ {
+ name: "non-existent parent dir creation",
+ config: Config{
+ DBRootDir: "/dir/does/not/exist/db3",
+ },
+ expected: map[string]string{
+ "file.txt": "new content",
+ },
+ init: func(t *testing.T, dir string, dbDir string) afero.Fs {
+ fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir())
+ require.NoError(t, fs.MkdirAll(dir, 0700))
+ require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "file.txt"), []byte("new content"), 0644))
+ return fs
+ },
+ },
+ {
+ name: "error during rename",
+ config: Config{
+ DBRootDir: "/test",
+ },
+ expected: nil, // no files expected since operation fails
+ init: func(t *testing.T, dir string, dbDir string) afero.Fs {
+ fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir())
+ require.NoError(t, fs.MkdirAll(dir, 0700))
+ require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "file.txt"), []byte("content"), 0644))
+ return afero.NewReadOnlyFs(fs)
+ },
+ wantErr: require.Error,
+ verify: func(t *testing.T, fs afero.Fs, config Config, expected map[string]string) {
+ _, err := fs.Stat(config.DBDirectoryPath())
+ require.Error(t, err)
+ },
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ if tc.wantErr == nil {
+ tc.wantErr = require.NoError
+ }
+ dbDir := tc.config.DBDirectoryPath()
+ candidateDir := "/temp/db"
+ fs := tc.init(t, candidateDir, dbDir)
+
+ c := curator{
+ fs: fs,
+ config: tc.config,
+ }
+
+ err := c.replaceDB(candidateDir)
+ tc.wantErr(t, err)
+ if tc.verify != nil {
+ tc.verify(t, fs, tc.config, tc.expected)
+ }
+ if err != nil {
+ return
+ }
+ for fileName, expectedContent := range tc.expected {
+ filePath := filepath.Join(tc.config.DBDirectoryPath(), fileName)
+ actualContent, err := afero.ReadFile(fs, filePath)
+ assert.NoError(t, err)
+ assert.Equal(t, expectedContent, string(actualContent))
+ }
+ })
+ }
+}
+func Test_isRehydrationNeeded(t *testing.T) {
+ tests := []struct {
+ name string
+ currentDBVersion schemaver.SchemaVer
+ hydrationClientVer schemaver.SchemaVer
+ currentClientVer schemaver.SchemaVer
+ expectedResult bool
+ expectedErr string
+ }{
+ {
+ name: "no database exists",
+ currentDBVersion: schemaver.SchemaVer{},
+ currentClientVer: schemaver.New(6, 2, 0),
+ expectedResult: false,
+ },
+ {
+ name: "no import metadata exists",
+ currentDBVersion: schemaver.New(6, 0, 0),
+ currentClientVer: schemaver.New(6, 2, 0),
+ expectedErr: "unable to read import metadata",
+ expectedResult: false,
+ },
+ {
+ name: "invalid client version in metadata",
+ currentDBVersion: schemaver.New(6, 0, 0),
+ hydrationClientVer: schemaver.SchemaVer{-19, 0, 0},
+ currentClientVer: schemaver.New(6, 2, 0),
+ expectedResult: false,
+ expectedErr: "unable to parse client version from import metadata",
+ },
+ {
+ name: "rehydration needed",
+ currentDBVersion: schemaver.New(6, 0, 1),
+ hydrationClientVer: schemaver.New(6, 0, 0),
+ currentClientVer: schemaver.New(6, 0, 2),
+ expectedResult: true,
+ },
+ {
+ name: "no rehydration needed - client version equals current client version",
+ currentDBVersion: schemaver.New(6, 0, 0),
+ hydrationClientVer: schemaver.New(6, 2, 0),
+ currentClientVer: schemaver.New(6, 2, 0),
+ expectedResult: false,
+ },
+ {
+ name: "no rehydration needed - client version greater than current client version",
+ currentDBVersion: schemaver.New(6, 0, 0),
+ hydrationClientVer: schemaver.New(6, 3, 0),
+ currentClientVer: schemaver.New(6, 2, 0),
+ expectedResult: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fs := afero.NewOsFs()
+ testDir := t.TempDir()
+
+ if tt.hydrationClientVer.Model != 0 {
+ writeTestImportMetadataWithCustomVersion(t, fs, testDir, "xxh64:something", tt.hydrationClientVer.String())
+ }
+
+ var dbVersion *schemaver.SchemaVer
+ if tt.currentDBVersion.Model != 0 {
+ dbVersion = &tt.currentDBVersion
+ }
+
+ result, err := isRehydrationNeeded(fs, testDir, dbVersion, tt.currentClientVer)
+
+ if tt.expectedErr != "" {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tt.expectedErr)
+ } else {
+ require.NoError(t, err)
+ assert.Equal(t, tt.expectedResult, result)
+ }
+ })
+ }
+}
+
+func TestCurator_Update_UsesDBRootDirForDownloadTempBase(t *testing.T) {
+ c := newTestCurator(t) // This sets up c.fs as afero.NewOsFs() rooted in t.TempDir()
+ mc := c.client.(*mockClient)
+
+ // This is the path that the mocked Download method will return.
+ // It simulates a temporary directory created by the download client within DBRootDir.
+ expectedDownloadedContentPath := filepath.Join(c.config.DBRootDir, "temp-downloaded-db-content-123")
+
+ // Pre-create this directory and make it look like a valid DB source for the hydrator and replaceDB.
+ require.NoError(t, c.fs.MkdirAll(expectedDownloadedContentPath, 0755))
+ // Write minimal valid DB metadata so that hydration/activation can proceed far enough.
+ // Using existing helpers to create a semblance of a DB.
+ writeTestDB(t, c.fs, expectedDownloadedContentPath) // This creates a basic DB file and import metadata.
+
+ // Mock client responses
+ mc.On("IsUpdateAvailable", mock.Anything).Return(&distribution.Archive{}, nil)
+ // CRUCIAL ASSERTION:
+ // Verify that Download is called with c.config.DBRootDir as its second argument (baseDirForTemp).
+ // It will return the expectedDownloadedContentPath, simulating successful download and extraction.
+ mc.On("Download", mock.Anything, c.config.DBRootDir, mock.Anything).Return(expectedDownloadedContentPath, nil)
+
+ hydrateCalled := false
+ c.hydrator = func(path string) error {
+ // Ensure hydrator is called with the path returned by Download
+ assert.Equal(t, expectedDownloadedContentPath, path, "hydrator called with incorrect path")
+ hydrateCalled = true
+ return nil // Simulate successful hydration
+ }
+
+ // Call Update to trigger the download and activation sequence
+ updated, err := c.Update()
+
+ // Assertions
+ require.NoError(t, err, "Update should succeed")
+ require.True(t, updated, "Update should report true")
+ mc.AssertExpectations(t) // Verifies that Download was called with the expected arguments
+ assert.True(t, hydrateCalled, "expected hydrator to be called")
+
+ // Check if the DB was "activated" (i.e., renamed)
+ finalDBPath := c.config.DBDirectoryPath()
+ _, err = c.fs.Stat(finalDBPath)
+ require.NoError(t, err, "final DB directory should exist after successful update")
+ // And the temporary downloaded content path should no longer exist as it was renamed
+ _, err = c.fs.Stat(expectedDownloadedContentPath)
+ require.True(t, os.IsNotExist(err), "temporary download path should not exist after rename")
+}
+
+func TestCurator_Update_CleansUpDownloadDirOnActivationFailure(t *testing.T) {
+ c := newTestCurator(t) // Sets up c.fs as afero.NewOsFs() rooted in t.TempDir()
+ mc := c.client.(*mockClient)
+
+ // This is the path that the mocked Download method will return.
+ // This directory should be cleaned up if activation fails.
+ downloadedContentPath := filepath.Join(c.config.DBRootDir, "temp-download-to-be-cleaned-up")
+
+ // Simulate the download client successfully creating this directory.
+ require.NoError(t, c.fs.MkdirAll(downloadedContentPath, 0755))
+ // Optionally, put a dummy file inside to make the cleanup more tangible.
+ require.NoError(t, afero.WriteFile(c.fs, filepath.Join(downloadedContentPath, "dummy_file.txt"), []byte("test data"), 0644))
+
+ // Mock client responses
+ mc.On("IsUpdateAvailable", mock.Anything).Return(&distribution.Archive{}, nil)
+ // Download is called with DBRootDir as base, and returns the path to the (simulated) downloaded content.
+ mc.On("Download", mock.Anything, c.config.DBRootDir, mock.Anything).Return(downloadedContentPath, nil)
+
+ // Configure the hydrator to fail, which will cause c.activate() to fail.
+ expectedHydrationError := "simulated hydration failure"
+ c.hydrator = func(path string) error {
+ assert.Equal(t, downloadedContentPath, path, "hydrator called with incorrect path")
+ return errors.New(expectedHydrationError)
+ }
+
+ // Call Update, expecting it to fail during activation.
+ updated, err := c.Update()
+
+ // Assertions
+ require.Error(t, err, "Update should fail due to activation error")
+ require.Contains(t, err.Error(), expectedHydrationError, "Error message should reflect hydration failure")
+ require.False(t, updated, "Update should report false on failure")
+ mc.AssertExpectations(t) // Verifies Download was called as expected.
+
+ // CRUCIAL ASSERTION:
+ // Verify that the temporary download directory was cleaned up.
+ _, statErr := c.fs.Stat(downloadedContentPath)
+ require.True(t, os.IsNotExist(statErr), "expected temporary download directory to be cleaned up after activation failure")
+}
+
+// Test for the Import path (URL case) - very similar to the Update tests
+func TestCurator_Import_URL_UsesDBRootDirForDownloadTempBaseAndCleansUp(t *testing.T) {
+ t.Run("successful import from URL", func(t *testing.T) {
+ c := newTestCurator(t)
+ mc := c.client.(*mockClient)
+
+ importURL := "http://localhost/some/db.tar.gz"
+ expectedDownloadedContentPath := filepath.Join(c.config.DBRootDir, "temp-imported-db-content-url")
+
+ require.NoError(t, c.fs.MkdirAll(expectedDownloadedContentPath, 0755))
+ writeTestDB(t, c.fs, expectedDownloadedContentPath)
+
+ mc.On("Download", importURL, c.config.DBRootDir, mock.Anything).Return(expectedDownloadedContentPath, nil)
+
+ hydrateCalled := false
+ c.hydrator = func(path string) error {
+ assert.Equal(t, expectedDownloadedContentPath, path)
+ hydrateCalled = true
+ return nil
+ }
+
+ err := c.Import(importURL)
+
+ require.NoError(t, err)
+ mc.AssertExpectations(t)
+ assert.True(t, hydrateCalled)
+ _, err = c.fs.Stat(c.config.DBDirectoryPath())
+ require.NoError(t, err, "final DB directory should exist")
+ _, err = c.fs.Stat(expectedDownloadedContentPath)
+ require.True(t, os.IsNotExist(err), "temp import path should not exist after rename")
+ })
+
+ t.Run("import from URL fails activation", func(t *testing.T) {
+ c := newTestCurator(t)
+ mc := c.client.(*mockClient)
+
+ importURL := "http://localhost/some/other/db.tar.gz"
+ downloadedContentPath := filepath.Join(c.config.DBRootDir, "temp-imported-to-cleanup-url")
+
+ require.NoError(t, c.fs.MkdirAll(downloadedContentPath, 0755))
+ require.NoError(t, afero.WriteFile(c.fs, filepath.Join(downloadedContentPath, "dummy.txt"), []byte("test"), 0644))
+
+ mc.On("Download", importURL, c.config.DBRootDir, mock.Anything).Return(downloadedContentPath, nil)
+
+ expectedHydrationError := "simulated hydration failure for import"
+ c.hydrator = func(path string) error {
+ return errors.New(expectedHydrationError)
+ }
+
+ err := c.Import(importURL)
+
+ require.Error(t, err)
+ require.Contains(t, err.Error(), expectedHydrationError)
+ mc.AssertExpectations(t)
+
+ _, statErr := c.fs.Stat(downloadedContentPath)
+ require.True(t, os.IsNotExist(statErr), "expected temp import directory to be cleaned up")
+ })
+}
+
+func setupTestDB(t *testing.T, dbDir string) db.ReadWriter {
+ s, err := db.NewWriter(db.Config{
+ DBDirPath: dbDir,
+ })
+ require.NoError(t, err)
+
+ return s
+}
+
+func setupReadOnlyTestDB(t *testing.T, dbDir string) db.Reader {
+ s, err := db.NewReader(db.Config{
+ DBDirPath: dbDir,
+ })
+ require.NoError(t, err)
+
+ return s
+}
+
+func testConfig() Config {
+ return DefaultConfig(clio.Identification{
+ Name: "grype-test",
+ })
+}
diff --git a/grype/db/v6/log_dropped.go b/grype/db/v6/log_dropped.go
new file mode 100644
index 00000000000..d843ba67359
--- /dev/null
+++ b/grype/db/v6/log_dropped.go
@@ -0,0 +1,15 @@
+package v6
+
+import (
+ "github.com/anchore/go-logger"
+ "github.com/anchore/grype/internal/log"
+)
+
+// logDroppedVulnerability is a hook called when vulnerabilities are dropped from consideration in a vulnerability Provider,
+// this offers a convenient location to set a breakpoint
+func logDroppedVulnerability(vuln string, reason any, fields logger.Fields) {
+ fields["reason"] = reason
+ fields["vulnerability"] = vuln
+
+ log.WithFields(fields).Trace("dropped vulnerability")
+}
diff --git a/grype/db/v6/models.go b/grype/db/v6/models.go
new file mode 100644
index 00000000000..6c4ca1ede7c
--- /dev/null
+++ b/grype/db/v6/models.go
@@ -0,0 +1,785 @@
+package v6
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/OneOfOne/xxhash"
+ "gorm.io/gorm"
+
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+func Models() []any {
+ return []any{
+ // core data store
+ &Blob{},
+
+ // non-domain info
+ &DBMetadata{},
+
+ // data source info
+ &Provider{},
+
+ // vulnerability related search tables
+ &VulnerabilityHandle{},
+ &VulnerabilityAlias{},
+
+ // package related search tables
+ &AffectedPackageHandle{}, // join on package, operating system
+ &OperatingSystem{},
+ &OperatingSystemSpecifierOverride{},
+ &Package{},
+ &PackageSpecifierOverride{},
+
+ // CPE related search tables
+ &AffectedCPEHandle{}, // join on CPE
+ &Cpe{},
+
+ // decorations to vulnerability records
+ &KnownExploitedVulnerabilityHandle{},
+ &EpssHandle{},
+ &EpssMetadata{},
+ }
+}
+
+type ID int64
+
+// core data store //////////////////////////////////////////////////////
+
+type Blob struct {
+ ID ID `gorm:"column:id;primaryKey"`
+ Value string `gorm:"column:value;not null"`
+}
+
+func (b Blob) computeDigest() string {
+ h := xxhash.New64()
+ if _, err := h.Write([]byte(b.Value)); err != nil {
+ log.Errorf("unable to hash blob: %v", err)
+ panic(err)
+ }
+ return fmt.Sprintf("xxh64:%x", h.Sum(nil))
+}
+
+// non-domain info //////////////////////////////////////////////////////
+
+type DBMetadata struct {
+ BuildTimestamp *time.Time `gorm:"column:build_timestamp;not null"`
+ Model int `gorm:"column:model;not null"`
+ Revision int `gorm:"column:revision;not null"`
+ Addition int `gorm:"column:addition;not null"`
+}
+
+func newSchemaVerFromDBMetadata(m DBMetadata) schemaver.SchemaVer {
+ return schemaver.New(m.Model, m.Revision, m.Addition)
+}
+
+// data source info //////////////////////////////////////////////////////
+
+// Provider is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider
+// should be scoped to a specific vulnerability dataset, for instance, the "ubuntu" provider for all records from
+// Canonicals' Ubuntu Security Notices (for all Ubuntu distro versions).
+type Provider struct {
+ // ID of the Vunnel provider (or sub processor responsible for data records from a single specific source, e.g. "ubuntu")
+ ID string `gorm:"column:id;primaryKey"`
+
+ // Version of the Vunnel provider (or sub processor equivalent)
+ Version string `gorm:"column:version"`
+
+ // Processor is the name of the application that processed the data (e.g. "vunnel")
+ Processor string `gorm:"column:processor"`
+
+ // DateCaptured is the timestamp which the upstream data was pulled and processed
+ DateCaptured *time.Time `gorm:"column:date_captured"`
+
+ // InputDigest is a self describing hash (e.g. sha256:123... not 123...) of all data used by the provider to generate the vulnerability records
+ InputDigest string `gorm:"column:input_digest"`
+}
+
+func (p *Provider) String() string {
+ if p == nil {
+ return ""
+ }
+ date := "?"
+ if p.DateCaptured != nil {
+ date = p.DateCaptured.UTC().Format(time.RFC3339)
+ }
+ return fmt.Sprintf("%s@v%s from %s using %q at %s", p.ID, p.Version, p.Processor, p.InputDigest, date)
+}
+
+func (p *Provider) cacheKey() string {
+ return strings.ToLower(p.String())
+}
+
+func (p *Provider) tableName() string {
+ return cpesTableCacheKey
+}
+
+func (p *Provider) rowID() string {
+ return p.ID
+}
+
+func (p *Provider) setRowID(i string) {
+ p.ID = i
+}
+
+func (p *Provider) BeforeCreate(tx *gorm.DB) (err error) {
+ if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {
+ if existingID, ok := cacheInst.getString(p); ok {
+ p.setRowID(existingID)
+ }
+ return nil
+ }
+ return fmt.Errorf("provider creation is not supported")
+}
+
+func (p *Provider) AfterCreate(tx *gorm.DB) (err error) {
+ if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {
+ cacheInst.set(p)
+ }
+ return nil
+}
+
+// vulnerability related search tables //////////////////////////////////////////////////////
+
+// VulnerabilityHandle represents the pointer to the core advisory record for a single known vulnerability from a specific provider.
+// indexes: idx_vuln_provider_id: this is used --by-cve to find all vulnerabilities from the NVD provider
+type VulnerabilityHandle struct {
+ ID ID `gorm:"column:id;primaryKey"`
+
+ // Name is the unique name for the vulnerability (same as the decoded VulnerabilityBlob.ID)
+ Name string `gorm:"column:name;not null;index,collate:NOCASE;index:idx_vuln_provider_id,collate:NOCASE"`
+
+ // Status conveys the actionability of the current record (one of "active", "analyzing", "rejected", "disputed")
+ Status VulnerabilityStatus `gorm:"column:status;not null;index,collate:NOCASE"`
+
+ // PublishedDate is the date the vulnerability record was first published
+ PublishedDate *time.Time `gorm:"column:published_date;index"`
+
+ // ModifiedDate is the date the vulnerability record was last modified
+ ModifiedDate *time.Time `gorm:"column:modified_date;index"`
+
+ // WithdrawnDate is the date the vulnerability record was withdrawn
+ WithdrawnDate *time.Time `gorm:"column:withdrawn_date;index"`
+
+ ProviderID string `gorm:"column:provider_id;not null;index;index:idx_vuln_provider_id,collate:NOCASE"`
+ Provider *Provider `gorm:"foreignKey:ProviderID"`
+
+ BlobID ID `gorm:"column:blob_id;index,unique"`
+ BlobValue *VulnerabilityBlob `gorm:"-"`
+}
+
+func (v VulnerabilityHandle) String() string {
+ return fmt.Sprintf("%s/%s", v.Provider, v.Name)
+}
+
+func (v VulnerabilityHandle) getBlobValue() any {
+ if v.BlobValue == nil {
+ return nil // must return untyped nil or getBlobValue() == nil will always be false
+ }
+ return v.BlobValue
+}
+
+func (v *VulnerabilityHandle) setBlobID(id ID) {
+ v.BlobID = id
+}
+
+func (v VulnerabilityHandle) getBlobID() ID {
+ return v.BlobID
+}
+
+func (v *VulnerabilityHandle) setBlob(rawBlobValue []byte) error {
+ var blobValue VulnerabilityBlob
+ if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {
+ return fmt.Errorf("unable to unmarshal vulnerability blob value: %w", err)
+ }
+
+ v.BlobValue = &blobValue
+ return nil
+}
+
+func (v *VulnerabilityHandle) cacheKey() string {
+ provider := "none"
+ if v.Provider != nil {
+ provider = v.Provider.ID
+ }
+ return strings.ToLower(fmt.Sprintf("%s from %s with %d", v.Name, provider, v.BlobID))
+}
+
+func (v *VulnerabilityHandle) rowID() ID {
+ return v.ID
+}
+
+func (v *VulnerabilityHandle) tableName() string {
+ return vulnerabilitiesTableCacheKey
+}
+
+func (v *VulnerabilityHandle) setRowID(i ID) {
+ v.ID = i
+}
+
+func (v *VulnerabilityHandle) BeforeCreate(tx *gorm.DB) (err error) {
+ if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {
+ if existing, ok := cacheInst.getID(v); ok {
+ v.setRowID(existing)
+ }
+
+ return nil
+ }
+
+ return fmt.Errorf("vulnerability creation is not supported")
+}
+
+func (v *VulnerabilityHandle) AfterCreate(tx *gorm.DB) (err error) {
+ if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {
+ cacheInst.set(v)
+ }
+ return nil
+}
+
+type VulnerabilityAlias struct {
+ // Name is the unique name for the vulnerability
+ Name string `gorm:"column:name;primaryKey;index,collate:NOCASE"`
+
+ // Alias is an alternative name for the vulnerability that must be upstream from the Name (e.g if name is "RHSA-1234" then the upstream could be "CVE-1234-5678", but not the other way around)
+ Alias string `gorm:"column:alias;primaryKey;index,collate:NOCASE;not null"`
+}
+
+// package related search tables //////////////////////////////////////////////////////
+
+// AffectedPackageHandle represents a single package affected by the specified vulnerability. A package here is a
+// name within a known ecosystem, such as "python" or "golang". It is important to note that this table relates
+// vulnerabilities to resolved packages. There are cases when we have package identifiers but are not resolved to
+// packages; for example, when we have a CPE but not a clear understanding of the package ecosystem and authoritative
+// name (which might or might not be the product name in the CPE), in which case AffectedCPEHandle should be used.
+type AffectedPackageHandle struct {
+ ID ID `gorm:"column:id;primaryKey"`
+ VulnerabilityID ID `gorm:"column:vulnerability_id;index;not null"`
+ Vulnerability *VulnerabilityHandle `gorm:"foreignKey:VulnerabilityID"`
+
+ OperatingSystemID *ID `gorm:"column:operating_system_id;index"`
+ OperatingSystem *OperatingSystem `gorm:"foreignKey:OperatingSystemID"`
+
+ PackageID ID `gorm:"column:package_id;index"`
+ Package *Package `gorm:"foreignKey:PackageID"`
+
+ BlobID ID `gorm:"column:blob_id"`
+ BlobValue *AffectedPackageBlob `gorm:"-"`
+}
+
+func (aph AffectedPackageHandle) vulnerability() string {
+ if aph.Vulnerability != nil {
+ return aph.Vulnerability.Name
+ }
+ if aph.BlobValue != nil {
+ if len(aph.BlobValue.CVEs) > 0 {
+ return aph.BlobValue.CVEs[0]
+ }
+ }
+ return ""
+}
+
+func (aph AffectedPackageHandle) String() string {
+ var fields []string
+
+ if aph.BlobValue != nil {
+ v := aph.BlobValue.String()
+ if v != "" {
+ fields = append(fields, v)
+ }
+ }
+ if aph.OperatingSystem != nil {
+ fields = append(fields, fmt.Sprintf("os=%q", aph.OperatingSystem.String()))
+ } else {
+ fields = append(fields, fmt.Sprintf("os=%d", aph.OperatingSystemID))
+ }
+
+ if aph.Package != nil {
+ fields = append(fields, fmt.Sprintf("pkg=%q", aph.Package.String()))
+ } else {
+ fields = append(fields, fmt.Sprintf("pkg=%d", aph.PackageID))
+ }
+
+ if aph.Vulnerability != nil {
+ fields = append(fields, fmt.Sprintf("vuln=%q", aph.Vulnerability.String()))
+ } else {
+ fields = append(fields, fmt.Sprintf("vuln=%d", aph.VulnerabilityID))
+ }
+
+ return fmt.Sprintf("affectedPackage(%s)", strings.Join(fields, ", "))
+}
+
+func (aph AffectedPackageHandle) getBlobValue() any {
+ if aph.BlobValue == nil {
+ return nil // must return untyped nil or getBlobValue() == nil will always be false
+ }
+ return aph.BlobValue
+}
+
+func (aph *AffectedPackageHandle) setBlobID(id ID) {
+ aph.BlobID = id
+}
+
+func (aph AffectedPackageHandle) getBlobID() ID {
+ return aph.BlobID
+}
+
+func (aph *AffectedPackageHandle) setBlob(rawBlobValue []byte) error {
+ var blobValue AffectedPackageBlob
+ if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {
+ return fmt.Errorf("unable to unmarshal affected package blob value: %w", err)
+ }
+
+ aph.BlobValue = &blobValue
+ return nil
+}
+
+// Package represents a package name within a known ecosystem, such as "python" or "golang".
+type Package struct {
+ ID ID `gorm:"column:id;primaryKey"`
+
+ // Ecosystem is the tooling and language ecosystem that the package is released within
+ Ecosystem string `gorm:"column:ecosystem;index:idx_package,unique,collate:NOCASE"`
+
+ // Name is the name of the package within the ecosystem
+ Name string `gorm:"column:name;index:idx_package,unique,collate:NOCASE;index:idx_package_name,collate:NOCASE"`
+
+ // CPEs is the list of Common Platform Enumeration (CPE) identifiers that represent this package
+ CPEs []Cpe `gorm:"many2many:package_cpes;"`
+}
+
+func (p Package) String() string {
+ var cpes []string
+ for _, cpe := range p.CPEs {
+ cpes = append(cpes, cpe.String())
+ }
+ if p.Ecosystem != "" && p.Name != "" {
+ base := fmt.Sprintf("%s/%s", p.Ecosystem, p.Name)
+ if len(cpes) == 0 {
+ return base
+ }
+
+ return fmt.Sprintf("%s (%s)", base, strings.Join(cpes, ", "))
+ }
+
+ return strings.Join(cpes, ", ")
+}
+
+func (p Package) cacheKey() string {
+ if p.Ecosystem == "" && p.Name == "" {
+ return ""
+ }
+ // we're intentionally not including anything about CPEs here, since there is potentially a merge operation for
+ // packages with CPEs we cannot reason about packages with CPEs in the cache, they must always pass through.
+ return strings.ToLower(fmt.Sprintf("%s/%s", p.Ecosystem, p.Name))
+}
+
+func (p Package) rowID() ID {
+ return p.ID
+}
+
+func (p *Package) tableName() string {
+ return packagesTableCacheKey
+}
+
+func (p *Package) setRowID(i ID) {
+ p.ID = i
+}
+
+func (p *Package) BeforeCreate(tx *gorm.DB) (err error) { // nolint:gocognit
+ cacheInst, ok := cacheFromContext(tx.Statement.Context)
+ if !ok {
+ return fmt.Errorf("cache not found in context")
+ }
+
+ var existingPackage Package
+ err = tx.Preload("CPEs").Where("ecosystem = ? collate nocase AND name = ? collate nocase", p.Ecosystem, p.Name).First(&existingPackage).Error
+ if err == nil {
+ // package exists; merge CPEs
+ for _, newCPE := range p.CPEs {
+ var existingCPE Cpe
+
+ if existingID, ok := cacheInst.getID(&newCPE); ok {
+ if err := tx.Where("id = ?", existingID).First(&existingCPE).Error; err != nil {
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
+ return fmt.Errorf("failed to find CPE by ID %d: %w", existingID, err)
+ }
+ }
+ }
+
+ if existingCPE.ID != 0 {
+ // if the record already exists, then we should use the existing record
+ continue
+ }
+
+ // if the CPE does not exist, proceed with creating it
+ existingPackage.CPEs = append(existingPackage.CPEs, newCPE)
+
+ if err := tx.Create(&newCPE).Error; err != nil {
+ return fmt.Errorf("failed to create CPE %v for package %v: %w", newCPE, existingPackage, err)
+ }
+ }
+ // use the existing package instead of creating a new one
+ *p = existingPackage
+ return nil
+ }
+ return nil
+}
+
+func (p *Package) AfterCreate(tx *gorm.DB) (err error) {
+ if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {
+ cacheInst.set(p)
+ for _, cpe := range p.CPEs {
+ cacheInst.set(&cpe)
+ }
+ }
+ return nil
+}
+
+// PackageSpecifierOverride is a table that allows for overriding fields on v6.PackageSpecifier instances when searching for specific Packages.
+type PackageSpecifierOverride struct {
+ Ecosystem string `gorm:"column:ecosystem;primaryKey;index:pkg_ecosystem_idx,collate:NOCASE"`
+
+ // below are the fields that should be used as replacement for fields in the Packages table
+
+ ReplacementEcosystem *string `gorm:"column:replacement_ecosystem;primaryKey"`
+}
+
+// OperatingSystem represents a specific release of an operating system. The resolution of the version is
+// relative to the available data by the vulnerability data provider, so though there may be major.minor.patch OS
+// releases, there may only be data available for major.minor.
+type OperatingSystem struct {
+ ID ID `gorm:"column:id;primaryKey"`
+
+ // Name is the operating system family name (e.g. "debian")
+ Name string `gorm:"column:name;index:os_idx,unique;index,collate:NOCASE"`
+ ReleaseID string `gorm:"column:release_id;index:os_idx,unique;index,collate:NOCASE"`
+
+ // MajorVersion is the major version of a specific release (e.g. "10" for debian 10)
+ MajorVersion string `gorm:"column:major_version;index:os_idx,unique;index"`
+
+ // MinorVersion is the minor version of a specific release (e.g. "1" for debian 10.1)
+ MinorVersion string `gorm:"column:minor_version;index:os_idx,unique;index"`
+
+ // LabelVersion is an optional non-codename string representation of the version (e.g. "unstable" or for debian:sid)
+ LabelVersion string `gorm:"column:label_version;index:os_idx,unique;index,collate:NOCASE"`
+
+ // Codename is the codename of a specific release (e.g. "buster" for debian 10)
+ Codename string `gorm:"column:codename;index,collate:NOCASE"`
+}
+
+func (o *OperatingSystem) VersionNumber() string {
+ if o == nil {
+ return ""
+ }
+ if o.MinorVersion != "" {
+ return fmt.Sprintf("%s.%s", o.MajorVersion, o.MinorVersion)
+ }
+ return o.MajorVersion
+}
+
+func (o *OperatingSystem) Version() string {
+ if o == nil {
+ return ""
+ }
+
+ if o.LabelVersion != "" {
+ return o.LabelVersion
+ }
+
+ if o.MajorVersion != "" {
+ if o.MinorVersion != "" {
+ return fmt.Sprintf("%s.%s", o.MajorVersion, o.MinorVersion)
+ }
+ return o.MajorVersion
+ }
+
+ return o.Codename
+}
+
+func (o OperatingSystem) String() string {
+ return fmt.Sprintf("%s@%s", o.Name, o.Version())
+}
+
+func (o OperatingSystem) cacheKey() string {
+ return strings.ToLower(o.String())
+}
+
+func (o OperatingSystem) rowID() ID {
+ return o.ID
+}
+
+func (o *OperatingSystem) tableName() string {
+ return operatingSystemsTableCacheKey
+}
+
+func (o *OperatingSystem) setRowID(i ID) {
+ o.ID = i
+}
+
+func (o *OperatingSystem) clean() {
+ o.MajorVersion = strings.TrimLeft(o.MajorVersion, "0")
+ o.MinorVersion = strings.TrimLeft(o.MinorVersion, "0")
+}
+
+func (o *OperatingSystem) BeforeCreate(tx *gorm.DB) (err error) {
+ o.clean()
+
+ if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {
+ if existing, ok := cacheInst.getID(o); ok {
+ o.setRowID(existing)
+ }
+ return nil
+ }
+
+ return fmt.Errorf("OS creation is not supported")
+}
+
+func (o *OperatingSystem) AfterCreate(tx *gorm.DB) (err error) {
+ if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {
+ cacheInst.set(o)
+ }
+ return nil
+}
+
+// OperatingSystemSpecifierOverride is a table that allows for overriding fields on v6.OSSpecifier instances when searching for specific OperatingSystems.
+type OperatingSystemSpecifierOverride struct {
+ // Alias is an alternative name/ID for the operating system.
+ Alias string `gorm:"column:alias;primaryKey;index:os_alias_idx,collate:NOCASE"`
+
+ // Version is the matching version as found in the VERSION_ID field if the /etc/os-release file
+ Version string `gorm:"column:version;primaryKey"`
+
+ // VersionPattern is a regex pattern to match against the VERSION_ID field if the /etc/os-release file
+ VersionPattern string `gorm:"column:version_pattern;primaryKey"`
+
+ // Codename is the matching codename as found in the VERSION_CODENAME field if the /etc/os-release file
+ Codename string `gorm:"column:codename"`
+
+ // below are the fields that should be used as replacement for fields in the OperatingSystem table
+
+ ReplacementName *string `gorm:"column:replacement;primaryKey"`
+ ReplacementMajorVersion *string `gorm:"column:replacement_major_version;primaryKey"`
+ ReplacementMinorVersion *string `gorm:"column:replacement_minor_version;primaryKey"`
+ ReplacementLabelVersion *string `gorm:"column:replacement_label_version;primaryKey"`
+ Rolling bool `gorm:"column:rolling;primaryKey"`
+}
+
+func (os *OperatingSystemSpecifierOverride) BeforeCreate(_ *gorm.DB) (err error) {
+ if os.Version != "" && os.VersionPattern != "" {
+ return fmt.Errorf("cannot have both version and version_pattern set")
+ }
+
+ return nil
+}
+
+// CPE related search tables //////////////////////////////////////////////////////
+
+// AffectedCPEHandle represents a single CPE affected by the specified vulnerability. Note the CPEs in this table
+// must NOT be resolvable to Packages (use AffectedPackageHandle for that). This table is used when the CPE is known,
+// but we do not have a clear understanding of the package ecosystem or authoritative name, so we can still
+// find vulnerabilities by these identifiers but not assert they are related to an entry in the Packages table.
+type AffectedCPEHandle struct {
+ ID ID `gorm:"column:id;primaryKey"`
+ VulnerabilityID ID `gorm:"column:vulnerability_id;not null"`
+ Vulnerability *VulnerabilityHandle `gorm:"foreignKey:VulnerabilityID"`
+
+ CpeID ID `gorm:"column:cpe_id;index"`
+ CPE *Cpe `gorm:"foreignKey:CpeID"`
+
+ BlobID ID `gorm:"column:blob_id"`
+ BlobValue *AffectedPackageBlob `gorm:"-"`
+}
+
+func (ach AffectedCPEHandle) vulnerability() string {
+ if ach.Vulnerability != nil {
+ return ach.Vulnerability.Name
+ }
+ if ach.BlobValue != nil {
+ if len(ach.BlobValue.CVEs) > 0 {
+ return ach.BlobValue.CVEs[0]
+ }
+ }
+ return ""
+}
+
+func (ach AffectedCPEHandle) String() string {
+ var fields []string
+
+ if ach.BlobValue != nil {
+ v := ach.BlobValue.String()
+ if v != "" {
+ fields = append(fields, v)
+ }
+ }
+
+ if ach.CPE != nil {
+ fields = append(fields, fmt.Sprintf("cpe=%q", ach.CPE.String()))
+ } else {
+ fields = append(fields, fmt.Sprintf("cpe=%d", ach.CpeID))
+ }
+
+ if ach.Vulnerability != nil {
+ fields = append(fields, fmt.Sprintf("vuln=%q", ach.Vulnerability.String()))
+ } else {
+ fields = append(fields, fmt.Sprintf("vuln=%d", ach.VulnerabilityID))
+ }
+
+ return fmt.Sprintf("affectedCPE(%s)", strings.Join(fields, ", "))
+}
+
+func (ach AffectedCPEHandle) getBlobID() ID {
+ return ach.BlobID
+}
+
+func (ach AffectedCPEHandle) getBlobValue() any {
+ if ach.BlobValue == nil {
+ return nil // must return untyped nil or getBlobValue() == nil will always be false
+ }
+ return ach.BlobValue
+}
+
+func (ach *AffectedCPEHandle) setBlobID(id ID) {
+ ach.BlobID = id
+}
+
+func (ach *AffectedCPEHandle) setBlob(rawBlobValue []byte) error {
+ var blobValue AffectedPackageBlob
+ if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {
+ return fmt.Errorf("unable to unmarshal affected cpe blob value: %w", err)
+ }
+
+ ach.BlobValue = &blobValue
+ return nil
+}
+
+type Cpe struct {
+ // TODO: what about different CPE versions?
+ ID ID `gorm:"primaryKey"`
+
+ Part string `gorm:"column:part;not null;index:idx_cpe,unique,collate:NOCASE"`
+ Vendor string `gorm:"column:vendor;index:idx_cpe,unique,collate:NOCASE;index:idx_cpe_vendor,collate:NOCASE"`
+ Product string `gorm:"column:product;not null;index:idx_cpe,unique,collate:NOCASE;index:idx_cpe_product,collate:NOCASE"`
+ Edition string `gorm:"column:edition;index:idx_cpe,unique,collate:NOCASE"`
+ Language string `gorm:"column:language;index:idx_cpe,unique,collate:NOCASE"`
+ SoftwareEdition string `gorm:"column:software_edition;index:idx_cpe,unique,collate:NOCASE"`
+ TargetHardware string `gorm:"column:target_hardware;index:idx_cpe,unique,collate:NOCASE"`
+ TargetSoftware string `gorm:"column:target_software;index:idx_cpe,unique,collate:NOCASE"`
+ Other string `gorm:"column:other;index:idx_cpe,unique,collate:NOCASE"`
+
+ Packages []Package `gorm:"many2many:package_cpes;"`
+}
+
+func (c Cpe) String() string {
+ parts := []string{"cpe:2.3", c.Part, c.Vendor, c.Product, c.Edition, c.Language, c.SoftwareEdition, c.TargetHardware, c.TargetSoftware, c.Other}
+ for i, part := range parts {
+ if part == "" {
+ parts[i] = "*"
+ }
+ }
+ return strings.Join(parts, ":")
+}
+
+func (c *Cpe) cacheKey() string {
+ return strings.ToLower(c.String())
+}
+
+func (c *Cpe) tableName() string {
+ return cpesTableCacheKey
+}
+
+func (c *Cpe) rowID() ID {
+ return c.ID
+}
+
+func (c *Cpe) setRowID(i ID) {
+ c.ID = i
+}
+
+func (c *Cpe) BeforeCreate(tx *gorm.DB) (err error) {
+ cacheInst, ok := cacheFromContext(tx.Statement.Context)
+ if !ok {
+ return fmt.Errorf("CPE creation is not supported")
+ }
+ if existingID, ok := cacheInst.getID(c); ok {
+ var existing Cpe
+ result := tx.Where("id = ?", existingID).First(&existing)
+ if result.Error == nil {
+ // if the record already exists, then we should use the existing record
+ *c = existing
+ }
+
+ c.setRowID(existingID)
+ }
+ return nil
+}
+
+func (c *Cpe) AfterCreate(tx *gorm.DB) (err error) {
+ if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {
+ cacheInst.set(c)
+ }
+ return nil
+}
+
+// PackageCpe join table for the many-to-many relationship
+type PackageCpe struct {
+ PackageID ID `gorm:"primaryKey;column:package_id"`
+ CpeID ID `gorm:"primaryKey;column:cpe_id"`
+}
+
+func (PackageCpe) TableName() string {
+ // note: this value is referenced in multiple struct tags and must not be changed or removed
+ // without this override the table name would be both model names in alphabetical order: cpes_packages
+ return "package_cpes"
+}
+
+type KnownExploitedVulnerabilityHandle struct {
+ ID int64 `gorm:"primaryKey"`
+
+ Cve string `gorm:"column:cve;not null;index:kev_cve_idx,collate:NOCASE"`
+
+ BlobID ID `gorm:"column:blob_id"`
+ BlobValue *KnownExploitedVulnerabilityBlob `gorm:"-"`
+}
+
+func (v KnownExploitedVulnerabilityHandle) getBlobValue() any {
+ if v.BlobValue == nil {
+ return nil // must return untyped nil or getBlobValue() == nil will always be false
+ }
+ return v.BlobValue
+}
+
+func (v *KnownExploitedVulnerabilityHandle) setBlobID(id ID) {
+ v.BlobID = id
+}
+
+func (v KnownExploitedVulnerabilityHandle) getBlobID() ID {
+ return v.BlobID
+}
+
+func (v *KnownExploitedVulnerabilityHandle) setBlob(rawBlobValue []byte) error {
+ var blobValue KnownExploitedVulnerabilityBlob
+ if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {
+ return fmt.Errorf("unable to unmarshal KEV blob value: %w", err)
+ }
+
+ v.BlobValue = &blobValue
+ return nil
+}
+
+type EpssMetadata struct {
+ Date time.Time `gorm:"column:date;not null"`
+}
+
+type EpssHandle struct {
+ ID int64 `gorm:"primaryKey"`
+
+ Cve string `gorm:"column:cve;not null;index:epss_cve_idx,collate:NOCASE"`
+ Epss float64 `gorm:"column:epss;not null"`
+ Percentile float64 `gorm:"column:percentile;not null"`
+ Date time.Time `gorm:"-"` // note we do not store the date in this table since it is expected to be the same for all records, that is what EpssMetadata is for
+}
diff --git a/grype/db/v6/models_test.go b/grype/db/v6/models_test.go
new file mode 100644
index 00000000000..b20736045e1
--- /dev/null
+++ b/grype/db/v6/models_test.go
@@ -0,0 +1,128 @@
+package v6
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestOperatingSystemAlias_VersionMutualExclusivity(t *testing.T) {
+ db := setupTestStore(t).db
+
+ msg := "cannot have both version and version_pattern set"
+
+ tests := []struct {
+ name string
+ input *OperatingSystemSpecifierOverride
+ errMsg string
+ }{
+ {
+ name: "version and version_pattern are mutually exclusive",
+ input: &OperatingSystemSpecifierOverride{
+ Alias: "ubuntu",
+ Version: "20.04",
+ VersionPattern: "20.*",
+ },
+ errMsg: msg,
+ },
+ {
+ name: "only version is set",
+ input: &OperatingSystemSpecifierOverride{
+ Alias: "ubuntu",
+ Version: "20.04",
+ },
+ errMsg: "",
+ },
+ {
+ name: "only version_pattern is set",
+ input: &OperatingSystemSpecifierOverride{
+ Alias: "ubuntu",
+ VersionPattern: "20.*",
+ },
+ errMsg: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := db.Create(tt.input).Error
+ if tt.errMsg == "" {
+ assert.NoError(t, err)
+ } else {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tt.errMsg)
+ }
+ })
+ }
+}
+
+func TestOperatingSystem_VersionNumber(t *testing.T) {
+ tests := []struct {
+ name string
+ os *OperatingSystem
+ expectedResult string
+ }{
+ {
+ name: "nil OS",
+ os: nil,
+ expectedResult: "",
+ },
+ {
+ name: "major and minor versions",
+ os: &OperatingSystem{MajorVersion: "10", MinorVersion: "1"},
+ expectedResult: "10.1",
+ },
+ {
+ name: "major version only",
+ os: &OperatingSystem{MajorVersion: "10"},
+ expectedResult: "10",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expectedResult, tt.os.VersionNumber())
+ })
+ }
+}
+
+func TestOperatingSystem_Version(t *testing.T) {
+ tests := []struct {
+ name string
+ os *OperatingSystem
+ expectedResult string
+ }{
+ {
+ name: "nil OS",
+ os: nil,
+ expectedResult: "",
+ },
+ {
+ name: "label version",
+ os: &OperatingSystem{LabelVersion: "unstable"},
+ expectedResult: "unstable",
+ },
+ {
+ name: "major and minor versions",
+ os: &OperatingSystem{MajorVersion: "10", MinorVersion: "1"},
+ expectedResult: "10.1",
+ },
+ {
+ name: "major version only",
+ os: &OperatingSystem{MajorVersion: "10"},
+ expectedResult: "10",
+ },
+ {
+ name: "codename",
+ os: &OperatingSystem{Codename: "buster"},
+ expectedResult: "buster",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expectedResult, tt.os.Version())
+ })
+ }
+}
diff --git a/grype/db/v4/pkg/resolver/java/resolver.go b/grype/db/v6/name/java.go
similarity index 84%
rename from grype/db/v4/pkg/resolver/java/resolver.go
rename to grype/db/v6/name/java.go
index c5933d78ef6..76f3a50e8db 100644
--- a/grype/db/v4/pkg/resolver/java/resolver.go
+++ b/grype/db/v6/name/java.go
@@ -1,8 +1,7 @@
-package java
+package name
import (
"fmt"
- "strings"
grypePkg "github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/internal/log"
@@ -10,14 +9,14 @@ import (
"github.com/anchore/packageurl-go"
)
-type Resolver struct {
+type JavaResolver struct {
}
-func (r *Resolver) Normalize(name string) string {
- return strings.ToLower(name)
+func (r *JavaResolver) Normalize(name string) string {
+ return name
}
-func (r *Resolver) Resolve(p grypePkg.Package) []string {
+func (r *JavaResolver) Names(p grypePkg.Package) []string {
names := stringutil.NewStringSet()
// The current default for the Java ecosystem is to use a Maven-like identifier of the form
diff --git a/grype/db/v4/pkg/resolver/java/resolver_test.go b/grype/db/v6/name/java_test.go
similarity index 74%
rename from grype/db/v4/pkg/resolver/java/resolver_test.go
rename to grype/db/v6/name/java_test.go
index ade2c8a9e15..de379347e03 100644
--- a/grype/db/v4/pkg/resolver/java/resolver_test.go
+++ b/grype/db/v6/name/java_test.go
@@ -1,4 +1,4 @@
-package java
+package name
import (
"testing"
@@ -9,42 +9,45 @@ import (
grypePkg "github.com/anchore/grype/grype/pkg"
)
-func TestResolver_Normalize(t *testing.T) {
+func TestJavaResolver_Normalize(t *testing.T) {
tests := []struct {
- packageName string
- normalized string
+ name string
+ normalized string
}{
{
- packageName: "PyYAML",
- normalized: "pyyaml",
+ name: "PyYAML",
+ // note we are not lowercasing since the DB is case-insensitive for name columns
+ normalized: "PyYAML",
},
{
- packageName: "oslo.concurrency",
- normalized: "oslo.concurrency",
+ name: "oslo.concurrency",
+ normalized: "oslo.concurrency",
},
{
- packageName: "",
- normalized: "",
+ name: "",
+ normalized: "",
},
{
- packageName: "test---1",
- normalized: "test---1",
+ name: "test---1",
+ normalized: "test---1",
},
{
- packageName: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
- normalized: "abcd.-__.--.-___.__.--1234____----....xyzzz",
+ name: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
+ normalized: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
},
}
- resolver := Resolver{}
+ resolver := JavaResolver{}
for _, test := range tests {
- resolvedNames := resolver.Normalize(test.packageName)
- assert.Equal(t, resolvedNames, test.normalized)
+ t.Run(test.name, func(t *testing.T) {
+ resolvedNames := resolver.Normalize(test.name)
+ assert.Equal(t, resolvedNames, test.normalized)
+ })
}
}
-func TestResolver_Resolve(t *testing.T) {
+func TestJavaResolver_Names(t *testing.T) {
tests := []struct {
name string
pkg grypePkg.Package
@@ -53,10 +56,9 @@ func TestResolver_Resolve(t *testing.T) {
{
name: "both artifact and manifest 1",
pkg: grypePkg.Package{
- Name: "ABCD",
- Version: "1.2.3.4",
- Language: "java",
- MetadataType: "",
+ Name: "ABCD",
+ Version: "1.2.3.4",
+ Language: "java",
Metadata: grypePkg.JavaMetadata{
VirtualPath: "virtual-path-info",
PomArtifactID: "pom-ARTIFACT-ID-info",
@@ -64,7 +66,7 @@ func TestResolver_Resolve(t *testing.T) {
ManifestName: "main-section-name-info",
},
},
- resolved: []string{"pom-group-id-info:pom-artifact-id-info", "pom-group-id-info:main-section-name-info"},
+ resolved: []string{"pom-group-ID-info:pom-ARTIFACT-ID-info", "pom-group-ID-info:main-section-name-info"},
},
{
name: "both artifact and manifest 2",
@@ -165,11 +167,11 @@ func TestResolver_Resolve(t *testing.T) {
},
}
- resolver := Resolver{}
+ resolver := JavaResolver{}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- resolvedNames := resolver.Resolve(test.pkg)
+ resolvedNames := resolver.Names(test.pkg)
assert.ElementsMatch(t, resolvedNames, test.resolved)
})
}
diff --git a/grype/db/v4/pkg/resolver/python/resolver.go b/grype/db/v6/name/python.go
similarity index 75%
rename from grype/db/v4/pkg/resolver/python/resolver.go
rename to grype/db/v6/name/python.go
index 0145bf09518..650f4961a51 100644
--- a/grype/db/v4/pkg/resolver/python/resolver.go
+++ b/grype/db/v6/name/python.go
@@ -1,25 +1,24 @@
-package python
+package name
import (
"regexp"
- "strings"
grypePkg "github.com/anchore/grype/grype/pkg"
)
-type Resolver struct {
+type PythonResolver struct {
}
-func (r *Resolver) Normalize(name string) string {
+func (r *PythonResolver) Normalize(name string) string {
// Canonical naming of packages within python is defined by PEP 503 at
// https://peps.python.org/pep-0503/#normalized-names, and this code is derived from
// the official python implementation of canonical naming at
// https://packaging.pypa.io/en/latest/_modules/packaging/utils.html#canonicalize_name
- return strings.ToLower(regexp.MustCompile(`[-_.]+`).ReplaceAllString(name, "-"))
+ return regexp.MustCompile(`[-_.]+`).ReplaceAllString(name, "-")
}
-func (r *Resolver) Resolve(p grypePkg.Package) []string {
+func (r *PythonResolver) Names(p grypePkg.Package) []string {
// Canonical naming of packages within python is defined by PEP 503 at
// https://peps.python.org/pep-0503/#normalized-names, and this code is derived from
// the official python implementation of canonical naming at
diff --git a/grype/db/v6/name/python_test.go b/grype/db/v6/name/python_test.go
new file mode 100644
index 00000000000..151afb44896
--- /dev/null
+++ b/grype/db/v6/name/python_test.go
@@ -0,0 +1,45 @@
+package name
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPythonResolver_Normalize(t *testing.T) {
+ tests := []struct {
+ name string
+ normalized string
+ }{
+ {
+ name: "PyYAML",
+ // note we are not lowercasing since the DB is case-insensitive for name columns
+ normalized: "PyYAML",
+ },
+ {
+ name: "oslo.concurrency",
+ normalized: "oslo-concurrency",
+ },
+ {
+ name: "",
+ normalized: "",
+ },
+ {
+ name: "test---1",
+ normalized: "test-1",
+ },
+ {
+ name: "AbCd.-__.--.-___.__.--1234____----....XyZZZ",
+ normalized: "AbCd-1234-XyZZZ",
+ },
+ }
+
+ resolver := PythonResolver{}
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ resolvedNames := resolver.Normalize(test.name)
+ assert.Equal(t, resolvedNames, test.normalized)
+ })
+ }
+}
diff --git a/grype/db/v6/name/resolver.go b/grype/db/v6/name/resolver.go
new file mode 100644
index 00000000000..bbf4d0537a1
--- /dev/null
+++ b/grype/db/v6/name/resolver.go
@@ -0,0 +1,44 @@
+package name
+
+import (
+ grypePkg "github.com/anchore/grype/grype/pkg"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+type Resolver interface {
+ Normalize(string) string
+ Names(p grypePkg.Package) []string
+}
+
+func FromType(t syftPkg.Type) Resolver {
+ switch t {
+ case syftPkg.PythonPkg:
+ return &PythonResolver{}
+ case syftPkg.JavaPkg, syftPkg.JenkinsPluginPkg:
+ return &JavaResolver{}
+ }
+
+ return nil
+}
+
+func PackageNames(p grypePkg.Package) []string {
+ names := []string{p.Name}
+ r := FromType(p.Type)
+ if r == nil {
+ return names
+ }
+
+ parts := r.Names(p)
+ if len(parts) > 0 {
+ names = parts
+ }
+ return names
+}
+
+func Normalize(name string, pkgType syftPkg.Type) string {
+ r := FromType(pkgType)
+ if r != nil {
+ return r.Normalize(name)
+ }
+ return name
+}
diff --git a/grype/db/v6/provider_store.go b/grype/db/v6/provider_store.go
new file mode 100644
index 00000000000..0cfcfbf1d0e
--- /dev/null
+++ b/grype/db/v6/provider_store.go
@@ -0,0 +1,88 @@
+package v6
+
+import (
+ "fmt"
+ "sort"
+
+ "gorm.io/gorm"
+
+ "github.com/anchore/grype/internal/log"
+)
+
+type ProviderStoreReader interface {
+ GetProvider(name string) (*Provider, error)
+ AllProviders() ([]Provider, error)
+ fillProviders(handles []ref[string, Provider]) error
+}
+
+type ProviderStoreWriter interface {
+ AddProvider(p Provider) error
+}
+
+type providerStore struct {
+ db *gorm.DB
+}
+
+func newProviderStore(db *gorm.DB) *providerStore {
+ return &providerStore{
+ db: db,
+ }
+}
+
+func (s *providerStore) AddProvider(p Provider) error {
+ result := s.db.FirstOrCreate(&p)
+ if result.Error != nil {
+ return fmt.Errorf("failed to create provider record: %w", result.Error)
+ }
+
+ return nil
+}
+
+func (s *providerStore) GetProvider(name string) (*Provider, error) {
+ log.WithFields("name", name).Trace("fetching provider record")
+
+ var provider Provider
+ result := s.db.Where("id = ?", name).First(&provider)
+ if result.Error != nil {
+ return nil, fmt.Errorf("failed to fetch provider (name=%q): %w", name, result.Error)
+ }
+
+ return &provider, nil
+}
+
+func (s *providerStore) AllProviders() ([]Provider, error) {
+ log.Trace("fetching all provider records")
+
+ var providers []Provider
+ result := s.db.Find(&providers)
+ if result.Error != nil {
+ return nil, fmt.Errorf("failed to fetch all providers: %w", result.Error)
+ }
+
+ sort.Slice(providers, func(i, j int) bool {
+ return providers[i].ID < providers[j].ID
+ })
+
+ return providers, nil
+}
+
+func (s *providerStore) fillProviders(handles []ref[string, Provider]) error {
+ providers, err := s.AllProviders()
+ if err != nil {
+ return err
+ }
+
+ providerMap := make(map[string]*Provider)
+ for _, provider := range providers {
+ providerMap[provider.ID] = &provider
+ }
+
+ for _, handle := range handles {
+ if handle.id == nil {
+ continue
+ }
+ *handle.ref = providerMap[*handle.id]
+ }
+
+ return nil
+}
diff --git a/grype/db/v6/provider_store_test.go b/grype/db/v6/provider_store_test.go
new file mode 100644
index 00000000000..d22b4228c2c
--- /dev/null
+++ b/grype/db/v6/provider_store_test.go
@@ -0,0 +1,81 @@
+package v6
+
+import (
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestProviderStore(t *testing.T) {
+ now := time.Date(2021, 1, 1, 2, 3, 4, 5, time.UTC)
+ tests := []struct {
+ name string
+ providers []Provider
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "add new provider",
+ providers: []Provider{
+ {
+ ID: "ubuntu",
+ Version: "1.0",
+ Processor: "vunnel",
+ DateCaptured: &now,
+ InputDigest: "sha256:abcd1234",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ db := setupTestStore(t).db
+ s := newProviderStore(db)
+ if tt.wantErr == nil {
+ tt.wantErr = require.NoError
+ }
+ for i := range tt.providers {
+ p := tt.providers[i]
+ // note: we always write providers via the vulnerability handle (there is no store adder)
+ vuln := VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ Provider: &p,
+ }
+ isLast := i == len(tt.providers)-1
+ err := db.Create(&vuln).Error
+ if !isLast {
+ require.NoError(t, err)
+ continue
+ }
+
+ tt.wantErr(t, err)
+ if err != nil {
+ continue
+ }
+
+ provider, err := s.GetProvider(p.ID)
+ tt.wantErr(t, err)
+ if err != nil {
+ assert.Nil(t, provider)
+ return
+ }
+
+ require.NoError(t, err)
+ require.NotNil(t, provider)
+ if d := cmp.Diff(p, *provider); d != "" {
+ t.Errorf("unexpected provider (-want +got): %s", d)
+ }
+ }
+ })
+ }
+}
+
+func TestProviderStore_GetProvider(t *testing.T) {
+ s := newProviderStore(setupTestStore(t).db)
+ p, err := s.GetProvider("fake")
+ require.Error(t, err)
+ assert.Nil(t, p)
+}
diff --git a/grype/db/v6/refs.go b/grype/db/v6/refs.go
new file mode 100644
index 00000000000..6a2df8975ed
--- /dev/null
+++ b/grype/db/v6/refs.go
@@ -0,0 +1,81 @@
+package v6
+
+import (
+ "slices"
+
+ "gorm.io/gorm"
+)
+
+type ref[ID, T any] struct {
+ id *ID
+ ref **T
+}
+
+type idRef[T any] ref[ID, T]
+
+type refProvider[T, R any] func(*T) idRef[R]
+
+type idProvider[T any] func(*T) ID
+
+func fillRefs[T, R any](reader Reader, handles []*T, getRef refProvider[T, R], refID idProvider[R]) error {
+ if len(handles) == 0 {
+ return nil
+ }
+
+ // collect all ref locations and IDs
+ var refs []idRef[R]
+ var ids []ID
+ for i := range handles {
+ ref := getRef(handles[i])
+ if ref.id == nil {
+ continue
+ }
+ refs = append(refs, ref)
+ id := *ref.id
+ if slices.Contains(ids, id) {
+ continue
+ }
+ ids = append(ids, id)
+ }
+
+ // load a map with all id -> ref results
+ var values []R
+ tx := reader.(lowLevelReader).GetDB().Where("id IN (?)", ids)
+ err := tx.Find(&values).Error
+ if err != nil {
+ return err
+ }
+ refsByID := map[ID]*R{}
+ for i := range values {
+ v := &values[i]
+ id := refID(v)
+ refsByID[id] = v
+ }
+
+ // assign matching refs back to the object graph
+ for _, ref := range refs {
+ if ref.id == nil {
+ continue
+ }
+ incomingRef := refsByID[*ref.id]
+ *ref.ref = incomingRef
+ }
+
+ return nil
+}
+
+// ptrs returns a slice of pointers to each element in the provided slice
+func ptrs[T any](values []T) []*T {
+ if len(values) == 0 {
+ return nil
+ }
+ out := make([]*T, len(values))
+ for i := range values {
+ out[i] = &values[i]
+ }
+ return out
+}
+
+type lowLevelReader interface {
+ GetDB() *gorm.DB
+}
diff --git a/grype/db/v6/severity.go b/grype/db/v6/severity.go
new file mode 100644
index 00000000000..74b14fefe18
--- /dev/null
+++ b/grype/db/v6/severity.go
@@ -0,0 +1,139 @@
+package v6
+
+import (
+ "fmt"
+
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/cvss"
+ "github.com/anchore/grype/internal/log"
+)
+
+func extractSeverities(vuln *VulnerabilityHandle) (vulnerability.Severity, []vulnerability.Cvss, error) {
+ if vuln.BlobValue == nil {
+ return vulnerability.UnknownSeverity, nil, nil
+ }
+ sev := vulnerability.UnknownSeverity
+ if len(vuln.BlobValue.Severities) > 0 {
+ var err error
+ // grype DB v6+ will order the set of severities by rank, so we can just take the first one
+ sev, err = extractSeverity(vuln.BlobValue.Severities[0].Value)
+ if err != nil {
+ return vulnerability.UnknownSeverity, nil, fmt.Errorf("unable to extract severity: %w", err)
+ }
+ }
+ return sev, toCvss(vuln.BlobValue.Severities...), nil
+}
+
+func extractSeverity(severity any) (vulnerability.Severity, error) {
+ switch sev := severity.(type) {
+ case string:
+ return vulnerability.ParseSeverity(sev), nil
+ case CVSSSeverity:
+ metrics, err := cvss.ParseMetricsFromVector(sev.Vector)
+ if err != nil {
+ return vulnerability.UnknownSeverity, fmt.Errorf("unable to parse CVSS vector: %w", err)
+ }
+ if metrics == nil {
+ return vulnerability.UnknownSeverity, nil
+ }
+ return interpretCVSS(metrics.BaseScore, sev.Version), nil
+ default:
+ return vulnerability.UnknownSeverity, nil
+ }
+}
+
+func interpretCVSS(score float64, version string) vulnerability.Severity {
+ switch version {
+ case "2.0":
+ return interpretCVSSv2(score)
+ case "3.0", "3.1", "4.0":
+ return interpretCVSSv3Plus(score)
+ default:
+ return vulnerability.UnknownSeverity
+ }
+}
+
+func interpretCVSSv2(score float64) vulnerability.Severity {
+ if score < 0 {
+ return vulnerability.UnknownSeverity
+ }
+ if score == 0 {
+ return vulnerability.NegligibleSeverity
+ }
+ if score < 4.0 {
+ return vulnerability.LowSeverity
+ }
+ if score < 7.0 {
+ return vulnerability.MediumSeverity
+ }
+ if score <= 10.0 {
+ return vulnerability.HighSeverity
+ }
+ return vulnerability.UnknownSeverity
+}
+
+func interpretCVSSv3Plus(score float64) vulnerability.Severity {
+ if score < 0 {
+ return vulnerability.UnknownSeverity
+ }
+ if score == 0 {
+ return vulnerability.NegligibleSeverity
+ }
+ if score < 4.0 {
+ return vulnerability.LowSeverity
+ }
+ if score < 7.0 {
+ return vulnerability.MediumSeverity
+ }
+ if score < 9.0 {
+ return vulnerability.HighSeverity
+ }
+ if score <= 10.0 {
+ return vulnerability.CriticalSeverity
+ }
+ return vulnerability.UnknownSeverity
+}
+
+func toCvss(severities ...Severity) []vulnerability.Cvss {
+ //nolint:prealloc
+ var out []vulnerability.Cvss
+ for _, sev := range severities {
+ switch sev.Scheme {
+ case SeveritySchemeCVSS:
+ default:
+ // not a CVSS score
+ continue
+ }
+ cvssSev, ok := sev.Value.(CVSSSeverity)
+ if !ok {
+ // not a CVSS score
+ continue
+ }
+ var usedMetrics vulnerability.CvssMetrics
+ // though the DB has the base score, we parse the vector for all metrics
+ metrics, err := cvss.ParseMetricsFromVector(cvssSev.Vector)
+ if err != nil {
+ log.WithFields("vector", cvssSev.Vector, "error", err).Warn("unable to parse CVSS vector")
+ continue
+ }
+ if metrics != nil {
+ usedMetrics = *metrics
+ }
+
+ out = append(out, vulnerability.Cvss{
+ Source: sev.Source,
+ Type: legacyCVSSType(sev.Rank),
+ Version: cvssSev.Version,
+ Vector: cvssSev.Vector,
+ Metrics: usedMetrics,
+ })
+ }
+ return out
+}
+
+func legacyCVSSType(rank int) string {
+ if rank == 1 {
+ return "Primary"
+ }
+ return "Secondary"
+}
diff --git a/grype/db/v6/severity_test.go b/grype/db/v6/severity_test.go
new file mode 100644
index 00000000000..3b943d0f448
--- /dev/null
+++ b/grype/db/v6/severity_test.go
@@ -0,0 +1,286 @@
+package v6
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+func TestExtractSeverity(t *testing.T) {
+ tests := []struct {
+ name string
+ input any
+ expected vulnerability.Severity
+ expectedErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "string low severity",
+ input: "low",
+ expected: vulnerability.LowSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "string high severity",
+ input: "high",
+ expected: vulnerability.HighSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "string critical severity",
+ input: "critical",
+ expected: vulnerability.CriticalSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "string unknown severity",
+ input: "invalid",
+ expected: vulnerability.UnknownSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "CVSS v2 low severity",
+ input: CVSSSeverity{
+ Version: "2.0",
+ Vector: "AV:L/AC:L/Au:N/C:N/I:P/A:N",
+ },
+ expected: vulnerability.LowSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "CVSS v2 medium severity",
+ input: CVSSSeverity{
+ Version: "2.0",
+ Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:N",
+ },
+ expected: vulnerability.MediumSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "CVSS v2 high severity",
+ input: CVSSSeverity{
+ Version: "2.0",
+ Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P",
+ },
+ expected: vulnerability.HighSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "CVSS v3 negligible severity",
+ input: CVSSSeverity{
+ Version: "3.1",
+ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N",
+ },
+ expected: vulnerability.NegligibleSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "CVSS v3 critical severity",
+ input: CVSSSeverity{
+ Version: "3.1",
+ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
+ },
+ expected: vulnerability.CriticalSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "CVSS v4 critical severity",
+ input: CVSSSeverity{
+ Version: "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",
+ },
+ expected: vulnerability.CriticalSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "invalid CVSS vector",
+ input: CVSSSeverity{
+ Version: "3.1",
+ Vector: "INVALID",
+ },
+ expected: vulnerability.UnknownSeverity,
+ expectedErr: require.Error,
+ },
+ {
+ name: "invalid type",
+ input: 123,
+ expected: vulnerability.UnknownSeverity,
+ expectedErr: require.NoError,
+ },
+ {
+ name: "nil input",
+ input: nil,
+ expected: vulnerability.UnknownSeverity,
+ expectedErr: require.NoError,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := extractSeverity(tt.input)
+ tt.expectedErr(t, err)
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+func TestExtractSeverities(t *testing.T) {
+ tests := []struct {
+ name string
+ input *VulnerabilityHandle
+ expectedSev vulnerability.Severity
+ expectedCVSS []vulnerability.Cvss
+ expectedError require.ErrorAssertionFunc
+ }{
+ {
+ name: "nil blob",
+ input: &VulnerabilityHandle{BlobValue: nil},
+ expectedSev: vulnerability.UnknownSeverity,
+ expectedCVSS: nil,
+ expectedError: require.NoError,
+ },
+ {
+ name: "empty severities",
+ input: &VulnerabilityHandle{
+ BlobValue: &VulnerabilityBlob{
+ Severities: []Severity{},
+ },
+ },
+ expectedSev: vulnerability.UnknownSeverity,
+ expectedCVSS: nil,
+ expectedError: require.NoError,
+ },
+ {
+ name: "valid primary CVSS severity",
+ input: &VulnerabilityHandle{
+ BlobValue: &VulnerabilityBlob{
+ Severities: []Severity{
+ {
+ Scheme: SeveritySchemeCVSS,
+ Source: "NVD",
+ Value: CVSSSeverity{
+ Version: "3.1",
+ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ },
+ Rank: 1,
+ },
+ },
+ },
+ },
+ expectedSev: vulnerability.CriticalSeverity,
+ expectedCVSS: []vulnerability.Cvss{
+ {
+ Source: "NVD",
+ Type: "Primary",
+ Version: "3.1",
+ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ Metrics: vulnerability.CvssMetrics{
+ BaseScore: 9.8,
+ ExploitabilityScore: ptr(3.9),
+ ImpactScore: ptr(5.9),
+ },
+ },
+ },
+ expectedError: require.NoError,
+ },
+ {
+ name: "valid secondary CVSS severity",
+ input: &VulnerabilityHandle{
+ BlobValue: &VulnerabilityBlob{
+ Severities: []Severity{
+ {
+ Scheme: SeveritySchemeCVSS,
+ Source: "NVD",
+ Value: CVSSSeverity{
+ Version: "3.1",
+ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ },
+ Rank: 2,
+ },
+ },
+ },
+ },
+ expectedSev: vulnerability.CriticalSeverity,
+ expectedCVSS: []vulnerability.Cvss{
+ {
+ Source: "NVD",
+ Type: "Secondary",
+ Version: "3.1",
+ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ Metrics: vulnerability.CvssMetrics{
+ BaseScore: 9.8,
+ ExploitabilityScore: ptr(3.9),
+ ImpactScore: ptr(5.9),
+ },
+ },
+ },
+ expectedError: require.NoError,
+ },
+ {
+ name: "valid CVSS severity with unknown rank (default to secondary)",
+ input: &VulnerabilityHandle{
+ BlobValue: &VulnerabilityBlob{
+ Severities: []Severity{
+ {
+ Scheme: SeveritySchemeCVSS,
+ Source: "NVD",
+ Value: CVSSSeverity{
+ Version: "3.1",
+ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ },
+ Rank: 3,
+ },
+ },
+ },
+ },
+ expectedSev: vulnerability.CriticalSeverity,
+ expectedCVSS: []vulnerability.Cvss{
+ {
+ Source: "NVD",
+ Type: "Secondary",
+ Version: "3.1",
+ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ Metrics: vulnerability.CvssMetrics{
+ BaseScore: 9.8,
+ ExploitabilityScore: ptr(3.9),
+ ImpactScore: ptr(5.9),
+ },
+ },
+ },
+ expectedError: require.NoError,
+ },
+ {
+ name: "invalid CVSS vector",
+ input: &VulnerabilityHandle{
+ BlobValue: &VulnerabilityBlob{
+ Severities: []Severity{
+ {
+ Scheme: SeveritySchemeCVSS,
+ Value: CVSSSeverity{
+ Version: "3.1",
+ Vector: "INVALID",
+ },
+ },
+ },
+ },
+ },
+ expectedSev: vulnerability.UnknownSeverity,
+ expectedCVSS: nil,
+ expectedError: require.Error,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectedError == nil {
+ tt.expectedError = require.NoError
+ }
+ sev, cvss, err := extractSeverities(tt.input)
+ tt.expectedError(t, err)
+ assert.Equal(t, tt.expectedSev, sev)
+ assert.Equal(t, tt.expectedCVSS, cvss)
+ })
+ }
+}
diff --git a/grype/db/v6/store.go b/grype/db/v6/store.go
new file mode 100644
index 00000000000..336f89c9102
--- /dev/null
+++ b/grype/db/v6/store.go
@@ -0,0 +1,164 @@
+package v6
+
+import (
+ "fmt"
+ "strings"
+
+ "gorm.io/gorm"
+
+ "github.com/anchore/grype/internal/log"
+)
+
+type store struct {
+ *dbMetadataStore
+ *providerStore
+ *vulnerabilityStore
+ *affectedPackageStore
+ *affectedCPEStore
+ *vulnerabilityDecoratorStore
+ blobStore *blobStore
+ db *gorm.DB
+ config Config
+ empty bool
+ writable bool
+}
+
+func (s *store) GetDB() *gorm.DB {
+ return s.db
+}
+
+func (s *store) attachBlobValue(values ...blobable) error {
+ return s.blobStore.attachBlobValue(values...)
+}
+
+func InitialData() []any {
+ var data []any
+ os := KnownOperatingSystemSpecifierOverrides()
+ for i := range os {
+ data = append(data, &os[i])
+ }
+
+ p := KnownPackageSpecifierOverrides()
+ for i := range p {
+ data = append(data, &p[i])
+ }
+ return data
+}
+
+func newStore(cfg Config, empty, writable bool) (*store, error) {
+ var path string
+ if cfg.DBDirPath != "" {
+ path = cfg.DBFilePath()
+ }
+
+ db, err := NewLowLevelDB(path, empty, writable, cfg.Debug)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open db: %w", err)
+ }
+
+ metadataStore := newDBMetadataStore(db)
+
+ if empty {
+ if err := metadataStore.SetDBMetadata(); err != nil {
+ return nil, fmt.Errorf("failed to set db metadata: %w", err)
+ }
+ }
+
+ meta, err := metadataStore.GetDBMetadata()
+ 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()
+ }
+ 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)
+
+ bs := newBlobStore(db)
+ return &store{
+ dbMetadataStore: metadataStore,
+ providerStore: newProviderStore(db),
+ vulnerabilityStore: newVulnerabilityStore(db, bs),
+ affectedPackageStore: newAffectedPackageStore(db, bs),
+ affectedCPEStore: newAffectedCPEStore(db, bs),
+ vulnerabilityDecoratorStore: newVulnerabilityDecoratorStore(db, bs, dbVersion),
+ blobStore: bs,
+ db: db,
+ config: cfg,
+ empty: empty,
+ writable: writable,
+ }, nil
+}
+
+// Close closes the store and finalizes the blobs when the DB is open for writing. If open for reading, only closes the connection to the DB.
+func (s *store) Close() error {
+ if !s.writable || !s.empty {
+ d, err := s.db.DB()
+ if err == nil {
+ return d.Close()
+ }
+ // if not empty, this writable execution created indexes
+ return nil
+ }
+ log.Debug("closing store")
+
+ // drop all indexes, which saves a lot of space distribution-wise (these get re-created on running gorm auto-migrate)
+ if err := dropAllIndexes(s.db); err != nil {
+ return err
+ }
+
+ // compact the DB size
+ log.Debug("vacuuming database")
+ if err := s.db.Exec("VACUUM").Error; err != nil {
+ return fmt.Errorf("failed to vacuum: %w", err)
+ }
+
+ // since we are using riskier statements to optimize write speeds, do a last integrity check
+ log.Debug("running integrity check")
+ if err := s.db.Exec("PRAGMA integrity_check").Error; err != nil {
+ return fmt.Errorf("integrity check failed: %w", err)
+ }
+
+ d, err := s.db.DB()
+ if err != nil {
+ return err
+ }
+
+ return d.Close()
+}
+
+func dropAllIndexes(db *gorm.DB) error {
+ tables, err := db.Migrator().GetTables()
+ if err != nil {
+ return fmt.Errorf("failed to get tables: %w", err)
+ }
+
+ log.WithFields("tables", len(tables)).Debug("discovering indexes")
+
+ for _, table := range tables {
+ indexes, err := db.Migrator().GetIndexes(table)
+ if err != nil {
+ return fmt.Errorf("failed to get indexes for table %s: %w", table, err)
+ }
+
+ log.WithFields("table", table, "indexes", len(indexes)).Trace("dropping indexes")
+ for _, index := range indexes {
+ // skip auto-generated UNIQUE or PRIMARY KEY indexes (sqlite will not allow you to drop these without more major surgery)
+ if strings.HasPrefix(index.Name(), "sqlite_autoindex") {
+ log.WithFields("table", table, "index", index.Name()).Trace("skip dropping autoindex")
+ continue
+ }
+ log.WithFields("table", table, "index", index.Name()).Trace("dropping index")
+ if err := db.Migrator().DropIndex(table, index.Name()); err != nil {
+ return fmt.Errorf("failed to drop index %s on table %s: %w", index, table, err)
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/grype/db/v6/store_test.go b/grype/db/v6/store_test.go
new file mode 100644
index 00000000000..16261061a6f
--- /dev/null
+++ b/grype/db/v6/store_test.go
@@ -0,0 +1,78 @@
+package v6
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "gorm.io/gorm"
+)
+
+func TestStoreClose(t *testing.T) {
+
+ t.Run("readonly mode does nothing", func(t *testing.T) {
+ dir := t.TempDir()
+ s := setupTestStore(t, dir)
+ s.empty = false
+ s.writable = false
+
+ err := s.Close()
+ require.NoError(t, err)
+
+ // ensure the connection is no longer open
+ var indexes []string
+ s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)
+ assert.Empty(t, indexes)
+
+ // get a new connection (readonly)
+ s = setupReadOnlyTestStore(t, dir)
+
+ // ensure we have our indexes
+ indexes = nil
+ s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)
+ assert.NotEmpty(t, indexes)
+
+ })
+
+ t.Run("successful close in writable mode", func(t *testing.T) {
+ dir := t.TempDir()
+ s := setupTestStore(t, dir)
+
+ // ensure we have indexes to start with
+ var indexes []string
+ s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)
+ assert.NotEmpty(t, indexes)
+
+ err := s.Close()
+ require.NoError(t, err)
+
+ // get a new connection (readonly)
+ s = setupReadOnlyTestStore(t, dir)
+
+ // ensure all of our indexes were dropped
+ indexes = nil
+ s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)
+ 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/default_vulnerabilities.go b/grype/db/v6/testutil/default_vulnerabilities.go
new file mode 100644
index 00000000000..8937e38c41d
--- /dev/null
+++ b/grype/db/v6/testutil/default_vulnerabilities.go
@@ -0,0 +1,30 @@
+package dbtest
+
+import (
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/syft/syft/cpe"
+)
+
+func DefaultVulnerabilities() []vulnerability.Vulnerability {
+ return []vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2024-1234",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "asdf",
+ Constraint: version.MustGetConstraint("< 1.4", version.ApkFormat),
+ PackageQualifiers: nil,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:stuff:asdf:*:*:*:*:*:*:*:*", cpe.DeclaredSource),
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"1.4.0"},
+ State: vulnerability.FixStateFixed,
+ },
+ Advisories: []vulnerability.Advisory{},
+ RelatedVulnerabilities: nil,
+ },
+ }
+}
diff --git a/grype/db/v6/testutil/server.go b/grype/db/v6/testutil/server.go
new file mode 100644
index 00000000000..69626857db8
--- /dev/null
+++ b/grype/db/v6/testutil/server.go
@@ -0,0 +1,373 @@
+package dbtest
+
+import (
+ "archive/tar"
+ "bytes"
+ "crypto/sha256"
+ "database/sql"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/archiver/v3"
+ "github.com/anchore/grype/grype/db/v5/namespace"
+ distroNs "github.com/anchore/grype/grype/db/v5/namespace/distro"
+ "github.com/anchore/grype/grype/db/v5/namespace/language"
+ v6 "github.com/anchore/grype/grype/db/v6"
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/file"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+type ServerBuilder struct {
+ t *testing.T
+ dbContents []byte
+ DBFormat string
+ DBBuildTime time.Time
+ DBVersion schemaver.SchemaVer
+ Vulnerabilities []vulnerability.Vulnerability
+ LatestDoc *distribution.LatestDocument
+ ServerSubdir string
+ LatestDocFile string
+ RequestHandler http.HandlerFunc
+}
+
+func (s *ServerBuilder) SetDBBuilt(t time.Time) *ServerBuilder {
+ s.DBBuildTime = t
+ return s
+}
+
+func (s *ServerBuilder) SetDBVersion(major, minor, patch int) *ServerBuilder {
+ s.DBVersion = schemaver.New(major, minor, patch)
+ return s
+}
+
+func (s *ServerBuilder) WithHandler(handler http.HandlerFunc) *ServerBuilder {
+ s.RequestHandler = handler
+ return s
+}
+
+// NewServer creates a new test db server building a single database from the provided
+// vulnerabilities, along with a latest.json pointing to it, optionally with any properties
+// specified in the provided latest parameter
+func NewServer(t *testing.T) *ServerBuilder {
+ t.Helper()
+ return &ServerBuilder{
+ t: t,
+ DBFormat: "tar.zst",
+ DBBuildTime: time.Now(),
+ DBVersion: schemaver.New(6, 0, 0),
+ ServerSubdir: "databases/v6",
+ LatestDocFile: "latest.json",
+ Vulnerabilities: DefaultVulnerabilities(),
+ LatestDoc: &distribution.LatestDocument{
+ Status: "active",
+ Archive: distribution.Archive{
+ Description: v6.Description{},
+ },
+ },
+ }
+}
+
+// Start starts builds a database and starts a server with the current settings
+// if you need to rebuild a DB or modify the behavior, you can either set
+// a custom RequestHandler func or modify the settings and call Start() again.
+// Returns a URL to the latest.json file, e.g. http://127.0.0.1:5678/v6/latest.json
+func (s *ServerBuilder) Start() (url string) {
+ s.t.Helper()
+
+ serverSubdir := s.ServerSubdir
+ if serverSubdir != "" {
+ serverSubdir += "/"
+ }
+
+ contents := s.buildDB()
+ s.dbContents = pack(s.t, s.DBFormat, contents)
+
+ handler := http.NewServeMux()
+ handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if s.RequestHandler != nil {
+ rw := wrappedWriter{writer: w}
+ s.RequestHandler(&rw, r)
+ if rw.handled {
+ return
+ }
+ }
+
+ dbName := "vulnerability-db_v" + s.DBVersion.String()
+ archivePath := dbName + "." + s.DBFormat
+ switch r.RequestURI[1:] {
+ case serverSubdir + s.LatestDocFile:
+ latestDoc := *s.LatestDoc
+ latestDoc.Built.Time = s.DBBuildTime
+ 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:
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(s.dbContents)
+ default:
+ http.NotFound(w, r)
+ return
+ }
+ })
+ mockSrv := httptest.NewServer(handler)
+ s.t.Cleanup(func() {
+ mockSrv.Close()
+ })
+ return mockSrv.URL + "/" + serverSubdir + s.LatestDocFile
+}
+
+func sha(contents []byte) string {
+ digest, err := file.HashReader(bytes.NewReader(contents), sha256.New())
+ if err != nil {
+ panic(err)
+ }
+ return "sha256:" + digest
+}
+
+//nolint:funlen
+func (s *ServerBuilder) buildDB() []byte {
+ s.t.Helper()
+
+ tmp := s.t.TempDir()
+ w, err := v6.NewWriter(v6.Config{
+ DBDirPath: tmp,
+ })
+ require.NoError(s.t, err)
+
+ aWeekAgo := time.Now().Add(-7 * 24 * time.Hour)
+ twoWeeksAgo := time.Now().Add(-14 * 24 * time.Hour)
+
+ for _, v := range s.Vulnerabilities {
+ prov := &v6.Provider{
+ ID: "nvd",
+ Version: "1",
+ DateCaptured: &s.DBBuildTime,
+ }
+
+ var operatingSystem *v6.OperatingSystem
+ packageType := ""
+
+ ns, err := namespace.FromString(v.Namespace)
+ require.NoError(s.t, err)
+
+ d, _ := ns.(*distroNs.Namespace)
+ if d != nil {
+ packageType = string(d.DistroType())
+ operatingSystem = &v6.OperatingSystem{
+ Name: d.Provider(),
+ MajorVersion: strings.Split(d.Version(), ".")[0],
+ }
+ prov.ID = d.Provider()
+ }
+ lang, _ := ns.(*language.Namespace)
+ if lang != nil {
+ packageType = string(lang.Language())
+ }
+
+ prov.Processor = prov.ID + "-processor"
+ prov.InputDigest = sha([]byte(prov.ID))
+
+ vuln := &v6.VulnerabilityHandle{
+ ID: 0,
+ Name: v.ID,
+ Status: "",
+ PublishedDate: &twoWeeksAgo,
+ ModifiedDate: &aWeekAgo,
+ WithdrawnDate: nil,
+ ProviderID: prov.ID,
+ Provider: prov,
+ BlobID: 0,
+ BlobValue: &v6.VulnerabilityBlob{
+ ID: v.ID,
+ Assigners: []string{v.ID + "-assigner-1", v.ID + "-assigner-2"},
+ Description: v.ID + "-description",
+ References: []v6.Reference{
+ {
+ URL: "http://somewhere/" + v.ID,
+ Tags: []string{v.ID + "-tag-1", v.ID + "-tag-2"},
+ },
+ },
+ //Aliases: []string{"GHSA-" + v.ID},
+ Severities: []v6.Severity{
+ {
+ Scheme: v6.SeveritySchemeCVSS,
+ Value: "high",
+ Source: "",
+ Rank: 0,
+ },
+ },
+ },
+ }
+
+ err = w.AddVulnerabilities(vuln)
+ require.NoError(s.t, err)
+
+ var cpes []v6.Cpe
+ for _, cp := range v.CPEs {
+ require.NoError(s.t, err)
+ cpes = append(cpes, v6.Cpe{
+ Part: cp.Attributes.Part,
+ Vendor: cp.Attributes.Vendor,
+ Product: cp.Attributes.Product,
+ Edition: cp.Attributes.Edition,
+ Language: cp.Attributes.Language,
+ SoftwareEdition: cp.Attributes.SWEdition,
+ TargetHardware: cp.Attributes.TargetHW,
+ TargetSoftware: cp.Attributes.TargetSW,
+ Other: cp.Attributes.Other,
+ })
+ }
+
+ pkg := &v6.Package{
+ ID: 0,
+ Ecosystem: packageType,
+ Name: v.PackageName,
+ }
+
+ if prov.ID != "nvd" {
+ pkg.CPEs = cpes
+ } else {
+ for _, c := range cpes {
+ ac := &v6.AffectedCPEHandle{
+ Vulnerability: vuln,
+ CPE: &c,
+ BlobValue: &v6.AffectedPackageBlob{
+ Ranges: []v6.AffectedRange{
+ {
+ Version: toAffectedVersion(v.Constraint),
+ },
+ },
+ },
+ }
+
+ err = w.AddAffectedCPEs(ac)
+ require.NoError(s.t, err)
+ }
+ }
+
+ ap := &v6.AffectedPackageHandle{
+ ID: 0,
+ VulnerabilityID: 0,
+ Vulnerability: vuln,
+ OperatingSystemID: nil,
+ OperatingSystem: operatingSystem,
+ PackageID: 0,
+ Package: pkg,
+ BlobID: 0,
+ BlobValue: &v6.AffectedPackageBlob{
+ CVEs: nil,
+ Qualifiers: nil,
+ Ranges: []v6.AffectedRange{
+ {
+ Fix: nil,
+ Version: toAffectedVersion(v.Constraint),
+ },
+ },
+ },
+ }
+
+ err = w.AddAffectedPackages(ap)
+ require.NoError(s.t, err)
+ }
+
+ err = w.SetDBMetadata()
+ require.NoError(s.t, err)
+
+ err = w.Close()
+ require.NoError(s.t, err)
+
+ dbFile := filepath.Join(tmp, "vulnerability.db")
+
+ db, err := sql.Open("sqlite", dbFile)
+ require.NoError(s.t, err)
+
+ model := s.DBVersion.Model
+ revision := s.DBVersion.Revision
+ addition := s.DBVersion.Addition
+ _, err = db.Exec("update db_metadata set build_timestamp = ?, model = ?, revision = ?, addition = ?",
+ s.DBBuildTime, model, revision, addition)
+ require.NoError(s.t, err)
+
+ err = db.Close()
+ require.NoError(s.t, err)
+
+ contents, err := os.ReadFile(dbFile)
+ require.NoError(s.t, err)
+
+ return contents
+}
+
+func pack(t *testing.T, typ string, contents []byte) []byte {
+ if typ == "tar.zst" {
+ now := time.Now()
+ tarContents := bytes.Buffer{}
+ tw := tar.NewWriter(&tarContents)
+ err := tw.WriteHeader(&tar.Header{
+ Typeflag: tar.TypeReg,
+ Name: "vulnerability.db",
+ Size: int64(len(contents)),
+ Mode: 0777,
+ ModTime: now,
+ })
+ require.NoError(t, err)
+ _, err = tw.Write(contents)
+ require.NoError(t, err)
+ err = tw.Close()
+ require.NoError(t, err)
+
+ tarZstd := bytes.Buffer{}
+ err = archiver.NewZstd().Compress(&tarContents, &tarZstd)
+ require.NoError(t, err)
+
+ return tarZstd.Bytes()
+ }
+
+ panic("unsupported type: " + typ)
+}
+
+func toAffectedVersion(c version.Constraint) v6.AffectedVersion {
+ parts := strings.SplitN(c.String(), "(", 2)
+ if len(parts) < 2 {
+ return v6.AffectedVersion{
+ Constraint: strings.TrimSpace(parts[0]),
+ }
+ }
+ return v6.AffectedVersion{
+ Type: strings.TrimSpace(strings.Split(parts[1], ")")[0]),
+ Constraint: strings.TrimSpace(parts[0]),
+ }
+}
+
+type wrappedWriter struct {
+ writer http.ResponseWriter
+ handled bool
+}
+
+func (w *wrappedWriter) Header() http.Header {
+ w.handled = true
+ return w.writer.Header()
+}
+
+func (w *wrappedWriter) Write(contents []byte) (int, error) {
+ w.handled = true
+ return w.writer.Write(contents)
+}
+
+func (w *wrappedWriter) WriteHeader(statusCode int) {
+ w.handled = true
+ w.writer.WriteHeader(statusCode)
+}
diff --git a/grype/db/v6/testutil/server_test.go b/grype/db/v6/testutil/server_test.go
new file mode 100644
index 00000000000..8b55066906c
--- /dev/null
+++ b/grype/db/v6/testutil/server_test.go
@@ -0,0 +1,82 @@
+package dbtest_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ dbtest "github.com/anchore/grype/grype/db/v6/testutil"
+)
+
+func Test_NewServer(t *testing.T) {
+ tests := []struct {
+ name string
+ useDefault bool
+ serverSubdir string
+ }{
+ {
+ name: "default path",
+ useDefault: true,
+ },
+ {
+ name: "v6 path",
+ serverSubdir: "v6",
+ },
+ {
+ name: "root path",
+ serverSubdir: "",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ srv := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(-24 * time.Hour))
+ if !test.useDefault {
+ srv.ServerSubdir = test.serverSubdir
+ }
+
+ url := srv.Start() // one day ago
+ parts := strings.Split(url, "/")
+ urlPrefix := strings.Join(parts[:len(parts)-1], "/")
+
+ get := func(url string) (status int, contents []byte, readError error) {
+ resp, err := http.Get(url)
+ if resp.Body != nil {
+ defer func() { require.NoError(t, resp.Body.Close()) }()
+ }
+ require.NoError(t, err)
+ buf := bytes.Buffer{}
+ _, err = io.Copy(&buf, resp.Body)
+ return resp.StatusCode, buf.Bytes(), err
+ }
+
+ status, content, err := get(urlPrefix + "/latest.json")
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, status)
+
+ // should have a latest document at the given URL
+ var latest distribution.LatestDocument
+ require.NoError(t, json.Unmarshal(content, &latest))
+
+ relativeDb := latest.Archive.Path
+ require.NotEmpty(t, relativeDb)
+
+ // should have a db at the relative url in the latest doc
+ status, content, err = get(urlPrefix + "/" + relativeDb)
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, status)
+ require.NotEmpty(t, content)
+
+ // should have 404 at wrong URL
+ status, _, _ = get(urlPrefix + "/asdf")
+ require.Equal(t, http.StatusNotFound, status)
+ })
+ }
+}
diff --git a/grype/db/v6/vulnerability.go b/grype/db/v6/vulnerability.go
new file mode 100644
index 00000000000..c17f528bea9
--- /dev/null
+++ b/grype/db/v6/vulnerability.go
@@ -0,0 +1,333 @@
+package v6
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/scylladb/go-set/strset"
+
+ "github.com/anchore/grype/grype/pkg/qualifier"
+ "github.com/anchore/grype/grype/pkg/qualifier/platformcpe"
+ "github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/syft/syft/cpe"
+ "github.com/anchore/syft/syft/pkg"
+)
+
+const v5NvdNamespace = "nvd:cpe"
+
+func newVulnerabilityFromAffectedPackageHandle(affected AffectedPackageHandle, affectedRanges []AffectedRange) (*vulnerability.Vulnerability, error) {
+ packageName := ""
+ if affected.Package != nil {
+ packageName = affected.Package.Name
+ }
+
+ if affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil {
+ return nil, fmt.Errorf("nil data when attempting to create vulnerability from AffectedPackageHandle")
+ }
+
+ return newVulnerabilityFromParts(packageName, affected.Vulnerability, affected.BlobValue, affectedRanges, &affected, nil)
+}
+
+func newVulnerabilityFromAffectedCPEHandle(affected AffectedCPEHandle, affectedRanges []AffectedRange) (*vulnerability.Vulnerability, error) {
+ if affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil {
+ return nil, fmt.Errorf("nil data when attempting to create vulnerability from AffectedCPEHandle")
+ }
+ return newVulnerabilityFromParts(affected.CPE.Product, affected.Vulnerability, affected.BlobValue, affectedRanges, nil, &affected)
+}
+
+func newVulnerabilityFromParts(packageName string, vuln *VulnerabilityHandle, affected *AffectedPackageBlob, affectedRanges []AffectedRange, affectedPackageHandle *AffectedPackageHandle, affectedCpeHandle *AffectedCPEHandle) (*vulnerability.Vulnerability, error) {
+ if vuln.BlobValue == nil {
+ return nil, fmt.Errorf("vuln has no blob value: %+v", vuln)
+ }
+
+ constraint, err := getVersionConstraint(affectedRanges)
+ if err != nil {
+ return nil, nil
+ }
+
+ v5namespace := MimicV5Namespace(vuln, affectedPackageHandle)
+ return &vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: vuln.Name,
+ Namespace: v5namespace,
+ Internal: vuln, // just hold a reference to the vulnHandle for later use
+ },
+ PackageName: packageName,
+ PackageQualifiers: getPackageQualifiers(affected),
+ Constraint: constraint,
+ CPEs: toCPEs(affectedPackageHandle, affectedCpeHandle),
+ RelatedVulnerabilities: getRelatedVulnerabilities(vuln, affected),
+ Fix: toFix(affectedRanges),
+ Advisories: toAdvisories(affectedRanges),
+ Status: string(vuln.Status),
+ }, nil
+}
+
+func getVersionConstraint(affectedRanges []AffectedRange) (version.Constraint, error) {
+ var constraints []string
+ types := strset.New()
+ for _, r := range affectedRanges {
+ if r.Version.Constraint != "" {
+ if r.Version.Type != "" {
+ types.Add(r.Version.Type)
+ }
+
+ constraints = append(constraints, r.Version.Constraint)
+ }
+ }
+
+ if types.Size() > 1 {
+ log.WithFields("types", types.List()).Debug("multiple version formats found for a single vulnerability")
+ }
+
+ var ty string
+ if types.Size() >= 1 {
+ typeStrs := types.List()
+ sort.Strings(typeStrs)
+ ty = typeStrs[0]
+ }
+
+ versionFormat := version.ParseFormat(ty)
+ constraint, err := version.GetConstraint(strings.Join(constraints, ","), versionFormat)
+ if err != nil {
+ log.WithFields("error", err, "constraint", constraints).Debug("unable to parse constraint")
+ return nil, err
+ }
+ return constraint, nil
+}
+
+func getRelatedVulnerabilities(vuln *VulnerabilityHandle, affected *AffectedPackageBlob) []vulnerability.Reference {
+ cveSet := strset.New()
+ var relatedVulnerabilities []vulnerability.Reference
+ for _, alias := range vuln.BlobValue.Aliases {
+ if cveSet.Has(alias) || strings.EqualFold(vuln.Name, alias) {
+ continue
+ }
+ if !strings.HasPrefix(strings.ToLower(alias), "cve-") {
+ continue
+ }
+ relatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{
+ ID: alias,
+ Namespace: v5NvdNamespace,
+ })
+ cveSet.Add(alias)
+ }
+ if affected != nil {
+ for _, cve := range affected.CVEs {
+ if cveSet.Has(cve) || strings.EqualFold(vuln.Name, cve) {
+ continue
+ }
+ if !strings.HasPrefix(strings.ToLower(cve), "cve-") {
+ continue
+ }
+ relatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{
+ ID: cve,
+ Namespace: v5NvdNamespace,
+ })
+ cveSet.Add(cve)
+ }
+ }
+ return relatedVulnerabilities
+}
+
+func getPackageQualifiers(affected *AffectedPackageBlob) []qualifier.Qualifier {
+ if affected != nil {
+ return toPackageQualifiers(affected.Qualifiers)
+ }
+
+ return nil
+}
+
+// MimicV5Namespace returns the namespace for a given affected package based on what schema v5 did.
+//
+//nolint:funlen
+func MimicV5Namespace(vuln *VulnerabilityHandle, affected *AffectedPackageHandle) string {
+ 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
+ ver := affected.OperatingSystem.Version()
+ switch affected.OperatingSystem.Name {
+ case "amazon":
+ family = "amazonlinux"
+ case "mariner", "azurelinux":
+ fields := strings.Split(ver, ".")
+ major := fields[0]
+ switch len(fields) {
+ case 1:
+ ver = fmt.Sprintf("%s.0", major)
+ default:
+ ver = fmt.Sprintf("%s.%s", major, fields[1])
+ }
+ switch major {
+ case "1", "2":
+ family = "mariner"
+ default:
+ family = "azurelinux"
+ }
+ case "ubuntu":
+ if strings.Count(ver, ".") == 1 {
+ // convert 20.4 to 20.04
+ fields := strings.Split(ver, ".")
+ major, minor := fields[0], fields[1]
+ if len(minor) == 1 {
+ ver = fmt.Sprintf("%s.0%s", major, minor)
+ }
+ }
+ case "oracle":
+ family = "oraclelinux"
+ }
+
+ // provider fixes
+ pr := vuln.Provider.ID
+ if pr == "rhel" {
+ pr = "redhat"
+ }
+
+ // version fixes
+ switch vuln.Provider.ID {
+ case "rhel", "oracle":
+ // ensure we only keep the major version
+ ver = strings.Split(ver, ".")[0]
+ }
+
+ 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
+}
+
+func toPackageQualifiers(qualifiers *AffectedPackageQualifiers) []qualifier.Qualifier {
+ if qualifiers == nil {
+ return nil
+ }
+ var out []qualifier.Qualifier
+ for _, c := range qualifiers.PlatformCPEs {
+ out = append(out, platformcpe.New(c))
+ }
+ if qualifiers.RpmModularity != nil {
+ out = append(out, rpmmodularity.New(*qualifiers.RpmModularity))
+ }
+ return out
+}
+
+func toFix(affectedRanges []AffectedRange) vulnerability.Fix {
+ var state vulnerability.FixState
+ var versions []string
+ for _, r := range affectedRanges {
+ if r.Fix == nil {
+ continue
+ }
+ switch r.Fix.State {
+ case FixedStatus:
+ state = vulnerability.FixStateFixed
+ versions = append(versions, r.Fix.Version)
+ case NotAffectedFixStatus:
+ // TODO: not handled yet
+ case WontFixStatus:
+ if state != vulnerability.FixStateFixed {
+ state = vulnerability.FixStateWontFix
+ }
+ case NotFixedStatus:
+ if state != vulnerability.FixStateFixed {
+ state = vulnerability.FixStateNotFixed
+ }
+ }
+ }
+ if len(versions) == 0 && state == "" {
+ return vulnerability.Fix{}
+ }
+ return vulnerability.Fix{
+ Versions: versions,
+ State: state,
+ }
+}
+
+func toAdvisories(affectedRanges []AffectedRange) []vulnerability.Advisory {
+ var advisories []vulnerability.Advisory
+ for _, r := range affectedRanges {
+ if r.Fix == nil || r.Fix.Detail == nil {
+ continue
+ }
+ for _, urlRef := range r.Fix.Detail.References {
+ if urlRef.URL == "" {
+ continue
+ }
+ advisories = append(advisories, vulnerability.Advisory{
+ Link: urlRef.URL,
+ })
+ }
+ }
+
+ return advisories
+}
+
+func toCPEs(affectedPackageHandle *AffectedPackageHandle, affectedCPEHandle *AffectedCPEHandle) []cpe.CPE {
+ var out []cpe.CPE
+ var cpes []Cpe
+ if affectedPackageHandle != nil {
+ cpes = affectedPackageHandle.Package.CPEs
+ }
+ if affectedCPEHandle != nil && affectedCPEHandle.CPE != nil {
+ cpes = append(cpes, *affectedCPEHandle.CPE)
+ }
+ for _, c := range cpes {
+ out = append(out, cpe.CPE{
+ Attributes: cpe.Attributes{
+ Part: c.Part,
+ Vendor: c.Vendor,
+ Product: c.Product,
+ Version: cpe.Any,
+ Update: cpe.Any,
+ Edition: c.Edition,
+ SWEdition: c.SoftwareEdition,
+ TargetSW: c.TargetSoftware,
+ TargetHW: c.TargetHardware,
+ Other: c.Other,
+ Language: c.Language,
+ },
+ Source: "",
+ })
+ }
+ return out
+}
diff --git a/grype/db/v6/vulnerability_decorator_store.go b/grype/db/v6/vulnerability_decorator_store.go
new file mode 100644
index 00000000000..670f71fb499
--- /dev/null
+++ b/grype/db/v6/vulnerability_decorator_store.go
@@ -0,0 +1,209 @@
+package v6
+
+import (
+ "fmt"
+ "time"
+
+ "gorm.io/gorm"
+
+ "github.com/anchore/go-logger"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+type VulnerabilityDecoratorStoreWriter interface {
+ AddKnownExploitedVulnerabilities(...*KnownExploitedVulnerabilityHandle) error
+ AddEpss(...*EpssHandle) error
+}
+
+type VulnerabilityDecoratorStoreReader interface {
+ GetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error)
+ GetEpss(cve string) ([]EpssHandle, error)
+}
+
+type vulnerabilityDecoratorStore struct {
+ db *gorm.DB
+ blobStore *blobStore
+ kevEnabled bool
+ epssEnabled bool
+ epssDate *time.Time
+}
+
+func newVulnerabilityDecoratorStore(db *gorm.DB, bs *blobStore, dbVersion schemaver.SchemaVer) *vulnerabilityDecoratorStore {
+ minSupportedKEVClientVersion := schemaver.New(6, 0, 1)
+ minSupportedEPSSClientVersion := schemaver.New(6, 0, 2)
+ return &vulnerabilityDecoratorStore{
+ db: db,
+ blobStore: bs,
+ kevEnabled: dbVersion.GreaterOrEqualTo(minSupportedKEVClientVersion),
+ epssEnabled: dbVersion.GreaterOrEqualTo(minSupportedEPSSClientVersion),
+ }
+}
+
+func (s *vulnerabilityDecoratorStore) AddEpss(epss ...*EpssHandle) error {
+ if !s.epssEnabled {
+ // when populating a new DB any capability issues found should result in halting
+ return ErrDBCapabilityNotSupported
+ }
+
+ for i := range epss {
+ e := epss[i]
+
+ if err := s.db.Create(e).Error; err != nil {
+ return fmt.Errorf("unable to create EPSS: %w", err)
+ }
+
+ if err := s.setEPSSMetadata(e.Date); err != nil {
+ return fmt.Errorf("unable to set EPSS metadata: %w", err)
+ }
+ }
+ return nil
+}
+
+func (s *vulnerabilityDecoratorStore) setEPSSMetadata(date time.Time) error {
+ if !s.epssEnabled {
+ // when populating a new DB any capability issues found should result in halting
+ return ErrDBCapabilityNotSupported
+ }
+
+ if s.epssDate != nil {
+ if s.epssDate.Equal(date) {
+ return nil
+ }
+ return fmt.Errorf("observed multiple EPSS dates: current=%q new=%q", s.epssDate.String(), date.String())
+ }
+
+ log.Trace("writing EPSS metadata")
+
+ if err := s.db.Where("true").Delete(&EpssMetadata{}).Error; err != nil {
+ return fmt.Errorf("failed to delete existing EPSS metadata record: %w", err)
+ }
+
+ instance := &EpssMetadata{
+ Date: date,
+ }
+
+ if err := s.db.Create(instance).Error; err != nil {
+ return fmt.Errorf("failed to create EPSS metadata record: %w", err)
+ }
+
+ s.epssDate = &date
+ return nil
+}
+
+func (s *vulnerabilityDecoratorStore) getEPSSMetadata() (*EpssMetadata, error) {
+ log.Trace("fetching EPSS metadata")
+
+ var model EpssMetadata
+
+ result := s.db.First(&model)
+ return &model, result.Error
+}
+
+func (s *vulnerabilityDecoratorStore) GetEpss(cve string) ([]EpssHandle, error) {
+ if !s.epssEnabled {
+ // capability incompatibilities should gracefully degrade, returning no data or errors
+ return nil, nil
+ }
+
+ fields := logger.Fields{
+ "cve": cve,
+ }
+ start := time.Now()
+ var count int
+ defer func() {
+ fields["duration"] = time.Since(start)
+ fields["records"] = count
+ log.WithFields(fields).Trace("fetched EPSS records")
+ }()
+
+ var models []EpssHandle
+ var results []*EpssHandle
+
+ if s.epssDate == nil {
+ // fetch and cache the EPSS metadata
+ metadata, err := s.getEPSSMetadata()
+ if err != nil {
+ return nil, fmt.Errorf("unable to fetch EPSS metadata: %w", err)
+ }
+ s.epssDate = &metadata.Date
+ }
+
+ if err := s.db.Where("cve = ? collate nocase", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {
+ for _, r := range results {
+ r.Date = *s.epssDate
+ models = append(models, *r)
+ }
+
+ count += len(results)
+
+ return nil
+ }).Error; err != nil {
+ return models, fmt.Errorf("unable to fetch EPSS records: %w", err)
+ }
+
+ return models, nil
+}
+
+func (s *vulnerabilityDecoratorStore) AddKnownExploitedVulnerabilities(kevs ...*KnownExploitedVulnerabilityHandle) error {
+ if !s.kevEnabled {
+ // when populating a new DB any capability issues found should result in halting
+ return ErrDBCapabilityNotSupported
+ }
+
+ for i := range kevs {
+ k := kevs[i]
+ // this adds the blob value to the DB and sets the ID on the kev handle
+ if err := s.blobStore.addBlobable(k); err != nil {
+ return fmt.Errorf("unable to add KEV blob: %w", err)
+ }
+
+ if err := s.db.Create(k).Error; err != nil {
+ return fmt.Errorf("unable to create known exploited vulnerability: %w", err)
+ }
+ }
+ return nil
+}
+
+func (s *vulnerabilityDecoratorStore) GetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error) {
+ if !s.kevEnabled {
+ // capability incompatibilities should gracefully degrade, returning no data or errors
+ return nil, nil
+ }
+
+ fields := logger.Fields{
+ "cve": cve,
+ }
+ start := time.Now()
+ var count int
+ defer func() {
+ fields["duration"] = time.Since(start)
+ fields["records"] = count
+ log.WithFields(fields).Trace("fetched KEV records")
+ }()
+
+ var models []KnownExploitedVulnerabilityHandle
+ var results []*KnownExploitedVulnerabilityHandle
+
+ if err := s.db.Where("cve = ? collate nocase", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {
+ var blobs []blobable
+ for _, r := range results {
+ blobs = append(blobs, r)
+ }
+ if err := s.blobStore.attachBlobValue(blobs...); err != nil {
+ return fmt.Errorf("unable to attach KEV blobs: %w", err)
+ }
+
+ for _, r := range results {
+ models = append(models, *r)
+ }
+
+ count += len(results)
+
+ return nil
+ }).Error; err != nil {
+ return models, fmt.Errorf("unable to fetch KEV records: %w", err)
+ }
+
+ return models, nil
+}
diff --git a/grype/db/v6/vulnerability_decorator_store_test.go b/grype/db/v6/vulnerability_decorator_store_test.go
new file mode 100644
index 00000000000..7f94d8307d7
--- /dev/null
+++ b/grype/db/v6/vulnerability_decorator_store_test.go
@@ -0,0 +1,296 @@
+package v6
+
+import (
+ "slices"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/internal/schemaver"
+)
+
+func TestVulnerabilityDecoratorStore(t *testing.T) {
+ tests := []struct {
+ name string
+ kevEnabled bool
+ setupStore func(*vulnerabilityDecoratorStore) error
+ input []*KnownExploitedVulnerabilityHandle
+ expectError require.ErrorAssertionFunc
+ }{
+ {
+ name: "happy path - single KEV",
+ kevEnabled: true,
+ input: []*KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-2023-1234",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-1234",
+ VendorProject: "Test Vendor",
+ Product: "Test Product",
+ DateAdded: timeRef(time.Now()),
+ },
+ },
+ },
+ },
+ {
+ name: "happy path - multiple KEVs",
+ kevEnabled: true,
+ input: []*KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-2023-1234",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-1234",
+ VendorProject: "Vendor 1",
+ },
+ },
+ {
+ Cve: "CVE-2023-5678",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-5678",
+ VendorProject: "Vendor 2",
+ },
+ },
+ },
+ },
+ {
+ name: "error - KEV disabled",
+ kevEnabled: false,
+ input: []*KnownExploitedVulnerabilityHandle{{Cve: "CVE-2023-1234"}},
+ expectError: require.Error,
+ },
+ {
+ name: "duplicate CVEs (unexpected but allowed)",
+ kevEnabled: true,
+ input: []*KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-2023-1234",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-1234",
+ RequiredAction: "1",
+ },
+ },
+ {
+ Cve: "CVE-2023-1234",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-1234",
+ RequiredAction: "2",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectError == nil {
+ tt.expectError = require.NoError
+ }
+
+ db := setupTestStore(t).db
+ bs := newBlobStore(db)
+
+ s := &vulnerabilityDecoratorStore{
+ db: db,
+ blobStore: bs,
+ kevEnabled: tt.kevEnabled,
+ }
+
+ if tt.setupStore != nil {
+ require.NoError(t, tt.setupStore(s))
+ }
+
+ err := s.AddKnownExploitedVulnerabilities(tt.input...)
+ tt.expectError(t, err)
+ if err != nil {
+ return
+ }
+
+ var cves []string
+ for _, kev := range tt.input {
+ if !slices.Contains(cves, kev.Cve) {
+ cves = append(cves, kev.Cve)
+ }
+ }
+
+ var actual []*KnownExploitedVulnerabilityHandle
+ for _, cve := range cves {
+ intermediate, err := s.GetKnownExploitedVulnerabilities(cve)
+ require.NoError(t, err)
+ for i := range intermediate {
+ actual = append(actual, &intermediate[i])
+ }
+ }
+
+ for _, a := range actual {
+ assert.NotZero(t, a.ID)
+ assert.NotZero(t, a.BlobID)
+ }
+
+ if d := cmp.Diff(tt.input, actual); d != "" {
+ t.Errorf("unexpected known exploited vulnerabilities (-expected, +actual): %s", d)
+ }
+ })
+ }
+}
+
+func TestVulnerabilityDecoratorStore_AddKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) {
+ tests := []struct {
+ name string
+ dbVersion schemaver.SchemaVer
+ input []*KnownExploitedVulnerabilityHandle
+ expectEnabled bool
+ expectError require.ErrorAssertionFunc
+ expectedCount int
+ }{
+ {
+ name: "supported db version",
+ dbVersion: schemaver.New(6, 0, 1),
+ input: []*KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-2023-1234",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-1234",
+ VendorProject: "Test Vendor",
+ DateAdded: timeRef(time.Now()),
+ },
+ },
+ },
+ expectEnabled: true,
+ expectError: require.NoError,
+ expectedCount: 1,
+ },
+ {
+ name: "unsupported db version",
+ dbVersion: schemaver.New(6, 0, 0),
+ input: []*KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-2023-1234",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-1234",
+ },
+ },
+ },
+ expectEnabled: false,
+ expectError: require.Error,
+ expectedCount: 0,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectError == nil {
+ tt.expectError = require.NoError
+ }
+
+ db := setupTestStore(t).db
+ bs := newBlobStore(db)
+
+ s := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion)
+ assert.Equal(t, tt.expectEnabled, s.kevEnabled)
+
+ err := s.AddKnownExploitedVulnerabilities(tt.input...)
+ tt.expectError(t, err)
+ if err != nil {
+ return
+ }
+
+ results, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve)
+ require.NoError(t, err)
+ assert.Len(t, results, tt.expectedCount)
+ })
+ }
+}
+
+func TestVulnerabilityDecoratorStore_GetKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) {
+ tests := []struct {
+ name string
+ dbVersion schemaver.SchemaVer
+ input []*KnownExploitedVulnerabilityHandle
+ expectEnabled bool
+ expectError require.ErrorAssertionFunc
+ expectedCount int
+ }{
+ {
+ name: "supported db version",
+ dbVersion: schemaver.New(6, 0, 1),
+ input: []*KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-2023-1234",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-1234",
+ },
+ },
+ },
+ expectEnabled: true,
+ expectError: require.NoError,
+ expectedCount: 1,
+ },
+ {
+ name: "unsupported db version",
+ dbVersion: schemaver.New(6, 0, 0),
+ input: []*KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-2023-1234",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-1234",
+ },
+ },
+ },
+ expectEnabled: false,
+ expectError: require.NoError,
+ expectedCount: 0,
+ },
+ {
+ name: "future db version",
+ dbVersion: schemaver.New(6, 1, 0),
+ input: []*KnownExploitedVulnerabilityHandle{
+ {
+ Cve: "CVE-2023-1234",
+ BlobValue: &KnownExploitedVulnerabilityBlob{
+ Cve: "CVE-2023-1234",
+ },
+ },
+ },
+ expectEnabled: true,
+ expectError: require.NoError,
+ expectedCount: 1,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.expectError == nil {
+ tt.expectError = require.NoError
+ }
+
+ db := setupTestStore(t).db
+ bs := newBlobStore(db)
+
+ s := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion)
+ assert.Equal(t, tt.expectEnabled, s.kevEnabled)
+
+ // this is just to get around not being able to write entries...
+ supportedStore := newVulnerabilityDecoratorStore(db, bs, schemaver.New(6, 0, 1))
+ err := supportedStore.AddKnownExploitedVulnerabilities(tt.input...)
+ require.NoError(t, err)
+
+ results, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve)
+ tt.expectError(t, err)
+ assert.Len(t, results, tt.expectedCount)
+
+ if tt.expectedCount > 0 {
+ for _, result := range results {
+ assert.NotNil(t, result.BlobValue)
+ assert.Equal(t, tt.input[0].Cve, result.BlobValue.Cve)
+ }
+ }
+ })
+ }
+}
+
+func timeRef(t time.Time) *time.Time {
+ return &t
+}
diff --git a/grype/db/v6/vulnerability_provider.go b/grype/db/v6/vulnerability_provider.go
new file mode 100644
index 00000000000..6e012daac38
--- /dev/null
+++ b/grype/db/v6/vulnerability_provider.go
@@ -0,0 +1,607 @@
+package v6
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/go-multierror"
+ "github.com/iancoleman/strcase"
+ "github.com/scylladb/go-set/strset"
+
+ "github.com/anchore/go-logger"
+ "github.com/anchore/grype/grype/db/v6/name"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/syft/syft/cpe"
+ 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,
+ }
+}
+
+type vulnerabilityProvider struct {
+ reader Reader
+}
+
+var _ interface {
+ vulnerability.Provider
+} = (*vulnerabilityProvider)(nil)
+
+// Deprecated: vulnerability.Vulnerability objects now have metadata included
+func (vp vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) {
+ vuln, ok := ref.Internal.(*VulnerabilityHandle)
+ if !ok {
+ var err error
+ vuln, err = vp.fetchVulnerability(ref)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if vuln == nil {
+ log.WithFields("id", ref.ID, "namespace", ref.Namespace).Debug("unable to find vulnerability for given reference")
+ return &vulnerability.Metadata{
+ ID: ref.ID,
+ DataSource: strings.Split(ref.Namespace, ":")[0],
+ Namespace: ref.Namespace,
+ Severity: toSeverityString(vulnerability.UnknownSeverity),
+ }, nil
+ }
+
+ return vp.getVulnerabilityMetadata(vuln, ref.Namespace)
+}
+
+func (vp vulnerabilityProvider) getVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string) (*vulnerability.Metadata, error) {
+ cves := getCVEs(vuln)
+
+ 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 := vp.fetchEpss(cves)
+ if err != nil {
+ log.WithFields("id", vuln.Name, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch epss from vulnerability")
+ }
+
+ return newVulnerabilityMetadata(vuln, namespace, kevs, epss)
+}
+
+func newVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string, kevs []vulnerability.KnownExploited, epss []vulnerability.EPSS) (*vulnerability.Metadata, error) {
+ if vuln == nil {
+ return nil, nil
+ }
+
+ 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: firstReferenceURL(vuln),
+ Namespace: namespace,
+ Severity: toSeverityString(sev),
+ URLs: lastReferenceURLs(vuln),
+ Description: vuln.BlobValue.Description,
+ Cvss: cvss,
+ KnownExploited: kevs,
+ EPSS: epss,
+ }, nil
+}
+
+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 := vp.reader.GetVulnerabilities(&VulnerabilitySpecifier{Name: ref.ID, Providers: []string{provider}}, &GetVulnerabilityOptions{Preload: true})
+ if err != nil {
+ return nil, err
+ }
+ if len(vulns) > 0 {
+ return &vulns[0], nil
+ }
+ return nil, nil
+}
+
+func (vp vulnerabilityProvider) fetchKnownExploited(cves []string) ([]vulnerability.KnownExploited, error) {
+ var out []vulnerability.KnownExploited
+ var errs error
+ for _, cve := range cves {
+ kevs, err := vp.reader.GetKnownExploitedVulnerabilities(cve)
+ if err != nil {
+ errs = multierror.Append(errs, err)
+ continue
+ }
+ for _, kev := range kevs {
+ out = append(out, vulnerability.KnownExploited{
+ CVE: kev.Cve,
+ VendorProject: kev.BlobValue.VendorProject,
+ Product: kev.BlobValue.Product,
+ DateAdded: kev.BlobValue.DateAdded,
+ RequiredAction: kev.BlobValue.RequiredAction,
+ DueDate: kev.BlobValue.DueDate,
+ KnownRansomwareCampaignUse: kev.BlobValue.KnownRansomwareCampaignUse,
+ Notes: kev.BlobValue.Notes,
+ URLs: kev.BlobValue.URLs,
+ CWEs: kev.BlobValue.CWEs,
+ })
+ }
+ }
+ return out, errs
+}
+
+func (vp vulnerabilityProvider) fetchEpss(cves []string) ([]vulnerability.EPSS, error) {
+ var out []vulnerability.EPSS
+ var errs error
+ for _, cve := range cves {
+ entries, err := vp.reader.GetEpss(cve)
+ if err != nil {
+ errs = multierror.Append(errs, err)
+ continue
+ }
+ for _, entry := range entries {
+ out = append(out, vulnerability.EPSS{
+ CVE: entry.Cve,
+ EPSS: entry.Epss,
+ Percentile: entry.Percentile,
+ Date: entry.Date,
+ })
+ }
+ }
+ return out, errs
+}
+
+func (vp vulnerabilityProvider) PackageSearchNames(p pkg.Package) []string {
+ return name.PackageNames(p)
+}
+
+func (vp vulnerabilityProvider) Close() error {
+ return vp.reader.(io.Closer).Close()
+}
+
+//nolint:funlen,gocognit,gocyclo
+func (vp vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) {
+ if err := search.ValidateCriteria(criteria); err != nil {
+ return nil, err
+ }
+
+ var err error
+
+ var out []vulnerability.Vulnerability
+ for _, criteriaSet := range search.CriteriaIterator(criteria) {
+ var vulnSpecs VulnerabilitySpecifiers
+ var osSpecs OSSpecifiers
+ var pkgSpec *PackageSpecifier
+ var cpeSpec *cpe.Attributes
+ var pkgType syftPkg.Type
+
+ for i := 0; i < len(criteriaSet); i++ {
+ applied := false
+ switch c := criteriaSet[i].(type) {
+ case *search.PackageNameCriteria:
+ if pkgSpec == nil {
+ pkgSpec = &PackageSpecifier{}
+ }
+ pkgSpec.Name = c.PackageName
+ applied = true
+ case *search.EcosystemCriteria:
+ if pkgSpec == nil {
+ pkgSpec = &PackageSpecifier{}
+ }
+ // the v6 store normalizes ecosystems around the syft package type, so that field is preferred
+ switch {
+ case c.PackageType != "" && c.PackageType != syftPkg.UnknownPkg:
+ // prefer to match by a non-blank, known package type
+ pkgType = c.PackageType
+ 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:
+ vulnSpecs = append(vulnSpecs, VulnerabilitySpecifier{
+ Name: c.ID,
+ })
+ applied = true
+ case *search.CPECriteria:
+ if cpeSpec == nil {
+ cpeSpec = &cpe.Attributes{}
+ }
+ *cpeSpec = c.CPE.Attributes
+ if cpeSpec.Product == cpe.Any {
+ return nil, fmt.Errorf("must specify product to search by CPE; got: %s", c.CPE.Attributes.BindToFmtString())
+ }
+ if pkgSpec == nil {
+ pkgSpec = &PackageSpecifier{}
+ }
+ pkgSpec.CPE = &c.CPE.Attributes
+ applied = true
+ case *search.DistroCriteria:
+ for _, d := range c.Distros {
+ osSpecs = append(osSpecs, &OSSpecifier{
+ Name: d.Name(),
+ MajorVersion: d.MajorVersion(),
+ MinorVersion: d.MinorVersion(),
+ RemainingVersion: d.RemainingVersion(),
+ LabelVersion: d.Codename,
+ })
+ }
+ applied = true
+ }
+
+ // remove fully applied criteria from later checks
+ if applied {
+ criteriaSet = append(criteriaSet[0:i], criteriaSet[i+1:]...)
+ i--
+ }
+ }
+
+ if len(osSpecs) == 0 {
+ // we don't want to search across all distros, instead if the user did not specify a distro we should assume that
+ // they want to search across affected packages not associated with any distro.
+ osSpecs = append(osSpecs, NoOSSpecified)
+ }
+
+ // if there is an ecosystem provided and a name, we need to make certain that we're using the name normalization
+ // rules specific to the ecosystem before searching.
+ if pkgType != "" && pkgSpec.Name != "" {
+ pkgSpec.Name = name.Normalize(pkgSpec.Name, pkgType)
+ }
+
+ versionMatcher, remainingCriteria := splitConstraintMatcher(criteriaSet...)
+
+ var affectedPackages []AffectedPackageHandle
+ var affectedCPEs []AffectedCPEHandle
+
+ if pkgSpec != nil || len(vulnSpecs) > 0 {
+ affectedPackages, err = vp.reader.GetAffectedPackages(pkgSpec, &GetAffectedPackageOptions{
+ OSs: osSpecs,
+ Vulnerabilities: vulnSpecs,
+ PreloadBlob: true,
+ })
+ if err != nil {
+ if errors.Is(err, ErrOSNotPresent) {
+ log.WithFields("os", osSpecs).Debug("no OS found in the DB for the given criteria")
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ affectedPackages = filterAffectedPackageVersions(versionMatcher, affectedPackages)
+
+ // after filtering, read vulnerability data
+ if err = fillAffectedPackageHandles(vp.reader, ptrs(affectedPackages)); err != nil {
+ return nil, err
+ }
+ }
+
+ if cpeSpec != nil {
+ affectedCPEs, err = vp.reader.GetAffectedCPEs(cpeSpec, &GetAffectedCPEOptions{
+ Vulnerabilities: vulnSpecs,
+ PreloadBlob: true,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ affectedCPEs = filterAffectedCPEVersions(versionMatcher, affectedCPEs, cpeSpec)
+
+ // after filtering, read vulnerability data
+ 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 := vp.toVulnerabilities(affectedPackages, affectedCPEs)
+ if err != nil {
+ return nil, err
+ }
+
+ // filter vulnerabilities by any remaining criteria such as ByQualifiedPackages
+ vulns, err = vp.filterVulnerabilities(vulns, remainingCriteria...)
+ if err != nil {
+ return nil, err
+ }
+
+ out = append(out, vulns...)
+ }
+
+ return out, nil
+}
+
+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 {
+ continue // already run
+ }
+ matches, reason, err := c.MatchesVulnerability(v)
+ if !matches || err != nil {
+ fields := logger.Fields{
+ "vulnerability": v,
+ }
+ if err != nil {
+ fields["error"] = err
+ }
+
+ logDroppedVulnerability(v.ID, reason, fields)
+ return false, err
+ }
+ }
+ return true, nil
+ }
+ for i := 0; i < len(vulns); i++ {
+ matches, err := isMatch(vulns[i])
+ if err != nil {
+ return nil, err
+ }
+ if !matches {
+ vulns = append(vulns[0:i], vulns[i+1:]...)
+ i--
+ }
+ }
+ return vulns, nil
+}
+
+// toVulnerabilities takes fully-filled handles and returns all vulnerabilities from them
+func (vp vulnerabilityProvider) toVulnerabilities(packageHandles []AffectedPackageHandle, cpeHandles []AffectedCPEHandle) ([]vulnerability.Vulnerability, error) { //nolint:funlen,gocognit
+ var out []vulnerability.Vulnerability
+
+ metadataByCVE := make(map[string]*vulnerability.Metadata)
+
+ getMetadata := func(vuln *VulnerabilityHandle, namespace string) (*vulnerability.Metadata, error) {
+ if vuln == nil {
+ return nil, nil
+ }
+
+ if metadata, ok := metadataByCVE[vuln.Name]; ok {
+ return metadata, nil
+ }
+
+ metadata, err := vp.getVulnerabilityMetadata(vuln, namespace)
+ if err != nil {
+ return nil, err
+ }
+
+ metadataByCVE[vuln.Name] = metadata
+ return metadata, nil
+ }
+
+ for _, packageHandle := range packageHandles {
+ if packageHandle.BlobValue == nil {
+ log.Debugf("unable to find blobValue for %+v", packageHandle)
+ continue
+ }
+ v, err := newVulnerabilityFromAffectedPackageHandle(packageHandle, packageHandle.BlobValue.Ranges)
+ if err != nil {
+ return nil, err
+ }
+ if v == nil {
+ continue
+ }
+
+ 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 {
+ v.Metadata = meta
+ }
+
+ out = append(out, *v)
+ }
+
+ for _, c := range cpeHandles {
+ if c.BlobValue == nil {
+ log.Debugf("unable to find blobValue for %+v", c)
+ continue
+ }
+ v, err := newVulnerabilityFromAffectedCPEHandle(c, c.BlobValue.Ranges)
+ if err != nil {
+ return nil, err
+ }
+ if v == nil {
+ continue
+ }
+
+ 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 {
+ v.Metadata = meta
+ }
+
+ out = append(out, *v)
+ }
+
+ return out, nil
+}
+
+// splitConstraintMatcher returns a search.VersionConstraintMatcher from all search.VersionConstraintMatcher(s) in the criteria
+func splitConstraintMatcher(criteria ...vulnerability.Criteria) (search.VersionConstraintMatcher, []vulnerability.Criteria) {
+ var remaining []vulnerability.Criteria
+ var matcher search.VersionConstraintMatcher
+ for _, c := range criteria {
+ if nextMatcher, ok := c.(search.VersionConstraintMatcher); ok {
+ if matcher == nil {
+ matcher = nextMatcher
+ } else {
+ matcher = search.MultiConstraintMatcher(matcher, nextMatcher)
+ }
+ } else {
+ remaining = append(remaining, c)
+ }
+ }
+ return matcher, remaining
+}
+
+func filterAffectedPackageVersions(constraintMatcher search.VersionConstraintMatcher, packages []AffectedPackageHandle) []AffectedPackageHandle {
+ // no constraint matcher, just return all packages
+ if constraintMatcher == nil {
+ return packages
+ }
+ var out []AffectedPackageHandle
+ for packageIdx := 0; packageIdx < len(packages); packageIdx++ {
+ handle := packages[packageIdx]
+ vuln := handle.vulnerability()
+ allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue)
+ if !allDropped {
+ out = append(out, handle)
+ continue // keep this handle
+ }
+
+ reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", "))
+ f := make(logger.Fields)
+ if handle.Package != nil {
+ f["package"] = handle.Package.String()
+ } else {
+ f["affectedPackage"] = handle
+ }
+
+ logDroppedVulnerability(vuln, reason, f)
+ }
+ return out
+}
+
+func filterAffectedCPEVersions(constraintMatcher search.VersionConstraintMatcher, handles []AffectedCPEHandle, cpeSpec *cpe.Attributes) []AffectedCPEHandle {
+ // no constraint matcher, just return all packages
+ if constraintMatcher == nil {
+ return handles
+ }
+ var out []AffectedCPEHandle
+ for i := range handles {
+ handle := handles[i]
+ vuln := handle.vulnerability()
+ allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue)
+ if !allDropped {
+ out = append(out, handle)
+ continue // keep this handle
+ }
+
+ reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", "))
+ logDroppedVulnerability(vuln, reason, logger.Fields{
+ "cpe": cpeSpec.String(),
+ })
+ }
+ return out
+}
+
+// filterAffectedPackageRanges returns true if all ranges removed
+func filterAffectedPackageRanges(matcher search.VersionConstraintMatcher, b *AffectedPackageBlob) (bool, []string) {
+ var unmatchedConstraints []string
+ for _, r := range b.Ranges {
+ v := r.Version
+ format := version.ParseFormat(v.Type)
+ constraint, err := version.GetConstraint(v.Constraint, format)
+ if err != nil || constraint == nil {
+ log.WithFields("error", err, "constraint", v.Constraint, "format", v.Type).Debug("unable to parse constraint")
+ continue
+ }
+ matches, err := matcher.MatchesConstraint(constraint)
+ if err != nil {
+ log.WithFields("error", err, "constraint", v.Constraint, "format", v.Type).Debug("match constraint error")
+ }
+ if matches {
+ continue
+ }
+ unmatchedConstraints = append(unmatchedConstraints, v.Constraint)
+ }
+ return len(b.Ranges) == len(unmatchedConstraints), unmatchedConstraints
+}
+
+func toSeverityString(sev vulnerability.Severity) string {
+ return strcase.ToCamel(sev.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
+}
+
+func getCVEs(vuln *VulnerabilityHandle) []string {
+ var cves []string
+ set := strset.New()
+
+ addCVE := func(id string) {
+ lower := strings.ToLower(id)
+ if strings.HasPrefix(lower, "cve-") {
+ if !set.Has(lower) {
+ cves = append(cves, id)
+ set.Add(lower)
+ }
+ }
+ }
+
+ if vuln == nil {
+ return cves
+ }
+
+ addCVE(vuln.Name)
+
+ if vuln.BlobValue == nil {
+ return cves
+ }
+
+ addCVE(vuln.BlobValue.ID)
+
+ for _, alias := range vuln.BlobValue.Aliases {
+ addCVE(alias)
+ }
+
+ return cves
+}
diff --git a/grype/db/v6/vulnerability_provider_mocks_test.go b/grype/db/v6/vulnerability_provider_mocks_test.go
new file mode 100644
index 00000000000..339fe650fc5
--- /dev/null
+++ b/grype/db/v6/vulnerability_provider_mocks_test.go
@@ -0,0 +1,270 @@
+package v6
+
+import (
+ "encoding/hex"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ v5 "github.com/anchore/grype/grype/db/v5"
+ "github.com/anchore/grype/grype/db/v5/namespace"
+ distroNs "github.com/anchore/grype/grype/db/v5/namespace/distro"
+ "github.com/anchore/grype/grype/db/v5/namespace/language"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/syft/syft/cpe"
+)
+
+func testVulnerabilityProvider(t *testing.T) vulnerability.Provider {
+ t.Helper()
+ tmp := t.TempDir()
+ w, err := NewWriter(Config{
+ DBDirPath: tmp,
+ })
+ defer log.CloseAndLogError(w, tmp)
+ require.NoError(t, err)
+
+ aDayAgo := time.Now().Add(-1 * 24 * time.Hour)
+ aWeekAgo := time.Now().Add(-7 * 24 * time.Hour)
+ twoWeeksAgo := time.Now().Add(-14 * 24 * time.Hour)
+
+ debianProvider := &Provider{
+ ID: "debian",
+ Version: "1",
+ Processor: "debian-processor",
+ DateCaptured: &aDayAgo,
+ 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
+ {
+ PackageName: "neutron",
+ Namespace: "debian:distro:debian:8",
+ VersionConstraint: "< 2014.1.3-6",
+ ID: "CVE-2014-fake-1",
+ VersionFormat: "deb",
+ },
+ {
+ PackageName: "neutron",
+ Namespace: "debian:distro:debian:8",
+ VersionConstraint: "< 2013.0.2-1",
+ ID: "CVE-2013-fake-2",
+ VersionFormat: "deb",
+ },
+ // poison the well! this is not a valid entry, but we want the matching process to survive and find other good results...
+ {
+ PackageName: "neutron",
+ Namespace: "debian:distro:debian:8",
+ VersionConstraint: "< 70.3.0-rc0", // intentionally bad value
+ ID: "CVE-2014-fake-3",
+ VersionFormat: "apk",
+ },
+
+ // activerecord
+ {
+ PackageName: "activerecord",
+ Namespace: "nvd:cpe",
+ VersionConstraint: "< 3.7.6",
+ ID: "CVE-2014-fake-3",
+ VersionFormat: "unknown",
+ CPEs: []string{
+ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
+ },
+ },
+ {
+ PackageName: "activerecord",
+ Namespace: "nvd:cpe",
+ VersionConstraint: "< 3.7.4",
+ ID: "CVE-2014-fake-4",
+ VersionFormat: "unknown",
+ CPEs: []string{
+ "cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*",
+ },
+ },
+ {
+ PackageName: "activerecord",
+ Namespace: "nvd:cpe",
+ VersionConstraint: "= 4.0.1",
+ ID: "CVE-2014-fake-5",
+ VersionFormat: "unknown",
+ CPEs: []string{
+ "cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*", // shouldn't match on this
+ },
+ },
+ {
+ PackageName: "activerecord",
+ Namespace: "nvd:cpe",
+ VersionConstraint: "< 98SP3",
+ ID: "CVE-2014-fake-6",
+ VersionFormat: "unknown",
+ CPEs: []string{
+ "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",
+ Namespace: "nvd:cpe",
+ VersionConstraint: "< 70.3.0-rc0", // intentionally bad value
+ ID: "CVE-2014-fake-7",
+ VersionFormat: "apk",
+ CPEs: []string{
+ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
+ },
+ },
+ }
+
+ 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",
+ }
+ }
+
+ vuln := &VulnerabilityHandle{
+ ID: 0,
+ Name: v.ID,
+ Status: "",
+ PublishedDate: &twoWeeksAgo,
+ ModifiedDate: &aWeekAgo,
+ WithdrawnDate: nil,
+ ProviderID: prov.ID,
+ Provider: prov,
+ BlobID: 0,
+ BlobValue: &VulnerabilityBlob{
+ ID: v.ID,
+ Assigners: []string{v.ID + "-assigner-1", v.ID + "-assigner-2"},
+ Description: v.ID + "-description",
+ References: []Reference{
+ {
+ URL: "http://somewhere/" + v.ID,
+ Tags: []string{v.ID + "-tag-1", v.ID + "-tag-2"},
+ },
+ },
+ //Aliases: []string{"GHSA-" + v.ID},
+ Severities: []Severity{
+ {
+ Scheme: SeveritySchemeCVSS,
+ Value: "high",
+ Source: "",
+ Rank: 0,
+ },
+ },
+ },
+ }
+
+ err = w.AddVulnerabilities(vuln)
+ require.NoError(t, err)
+
+ var cpes []Cpe
+ for _, c := range v.CPEs {
+ cp, err := cpe.New(c, "")
+ require.NoError(t, err)
+ cpes = append(cpes, Cpe{
+ Part: cp.Attributes.Part,
+ Vendor: cp.Attributes.Vendor,
+ Product: cp.Attributes.Product,
+ Edition: cp.Attributes.Edition,
+ Language: cp.Attributes.Language,
+ SoftwareEdition: cp.Attributes.SWEdition,
+ TargetHardware: cp.Attributes.TargetHW,
+ TargetSoftware: cp.Attributes.TargetSW,
+ Other: cp.Attributes.Other,
+ })
+ }
+
+ packageType := ""
+
+ ns, err := namespace.FromString(v.Namespace)
+ require.NoError(t, err)
+
+ d, _ := ns.(*distroNs.Namespace)
+ if d != nil {
+ packageType = string(d.DistroType())
+ }
+ lang, _ := ns.(*language.Namespace)
+ if lang != nil {
+ packageType = string(lang.Language())
+ }
+
+ pkg := &Package{
+ ID: 0,
+ Ecosystem: packageType,
+ Name: v.PackageName,
+ //CPEs: cpes,
+ }
+
+ ap := &AffectedPackageHandle{
+ ID: 0,
+ VulnerabilityID: 0,
+ Vulnerability: vuln,
+ OperatingSystemID: nil,
+ OperatingSystem: os,
+ PackageID: 0,
+ Package: pkg,
+ BlobID: 0,
+ BlobValue: &AffectedPackageBlob{
+ CVEs: nil,
+ Qualifiers: nil,
+ Ranges: []AffectedRange{
+ {
+ Fix: nil,
+ Version: AffectedVersion{
+ Type: v.VersionFormat,
+ Constraint: v.VersionConstraint,
+ },
+ },
+ },
+ },
+ }
+
+ err = w.AddAffectedPackages(ap)
+ require.NoError(t, err)
+
+ for _, c := range cpes {
+ ac := &AffectedCPEHandle{
+ Vulnerability: vuln,
+ CPE: &c,
+ BlobValue: &AffectedPackageBlob{
+ Ranges: []AffectedRange{
+ {
+ Version: AffectedVersion{
+ Type: v.VersionFormat,
+ Constraint: v.VersionConstraint,
+ },
+ },
+ },
+ },
+ }
+
+ err = w.AddAffectedCPEs(ac)
+ require.NoError(t, err)
+ }
+ }
+
+ return NewVulnerabilityProvider(setupReadOnlyTestStore(t, tmp))
+}
diff --git a/grype/db/v6/vulnerability_provider_test.go b/grype/db/v6/vulnerability_provider_test.go
new file mode 100644
index 00000000000..73b451ced82
--- /dev/null
+++ b/grype/db/v6/vulnerability_provider_test.go
@@ -0,0 +1,466 @@
+package v6
+
+import (
+ "testing"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/distro"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/pkg/qualifier"
+ "github.com/anchore/grype/grype/search"
+ "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) {
+ provider := testVulnerabilityProvider(t)
+
+ d, err := distro.New(distro.Debian, "8", "")
+ require.NoError(t, err)
+
+ p := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "neutron",
+ }
+
+ actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByPackageName(p.Name))
+ require.NoError(t, err)
+
+ expected := []vulnerability.Vulnerability{
+ {
+ PackageName: "neutron",
+ Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-1",
+ Namespace: "debian:distro:debian:8",
+ },
+ PackageQualifiers: []qualifier.Qualifier{},
+ CPEs: nil,
+ Advisories: []vulnerability.Advisory{},
+ Metadata: &vulnerability.Metadata{
+ ID: "CVE-2014-fake-1",
+ DataSource: "http://somewhere/CVE-2014-fake-1",
+ Namespace: "debian:distro:debian:8",
+ Severity: "High",
+ URLs: nil,
+ Description: "CVE-2014-fake-1-description",
+ },
+ },
+ {
+ PackageName: "neutron",
+ Constraint: version.MustGetConstraint("< 2013.0.2-1", version.DebFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2013-fake-2",
+ Namespace: "debian:distro:debian:8",
+ },
+ PackageQualifiers: []qualifier.Qualifier{},
+ CPEs: nil,
+ Advisories: []vulnerability.Advisory{},
+ Metadata: &vulnerability.Metadata{
+ ID: "CVE-2013-fake-2",
+ DataSource: "http://somewhere/CVE-2013-fake-2",
+ Namespace: "debian:distro:debian:8",
+ Severity: "High",
+ URLs: nil,
+ Description: "CVE-2013-fake-2-description",
+ },
+ },
+ }
+
+ require.Len(t, actual, len(expected))
+
+ for idx, vuln := range actual {
+ if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" {
+ t.Errorf("diff: %+v", d)
+ }
+ }
+}
+
+func Test_FindVulnerabilitiesByEmptyDistro(t *testing.T) {
+ provider := testVulnerabilityProvider(t)
+
+ p := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "neutron",
+ }
+
+ vulnerabilities, err := provider.FindVulnerabilities(search.ByDistro(distro.Distro{}), search.ByPackageName(p.Name))
+
+ require.Empty(t, vulnerabilities)
+ require.NoError(t, err)
+}
+
+func Test_FindVulnerabilitiesByCPE(t *testing.T) {
+
+ tests := []struct {
+ name string
+ cpe cpe.CPE
+ expected []vulnerability.Vulnerability
+ err bool
+ }{
+ {
+ name: "match from name and target SW",
+ cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*", ""),
+ expected: []vulnerability.Vulnerability{
+ {
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-4",
+ Namespace: "nvd:cpe",
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
+ },
+ PackageQualifiers: []qualifier.Qualifier{},
+ Advisories: []vulnerability.Advisory{},
+ Metadata: &vulnerability.Metadata{
+ ID: "CVE-2014-fake-4",
+ DataSource: "http://somewhere/CVE-2014-fake-4",
+ Namespace: "nvd:cpe",
+ Severity: "High",
+ URLs: nil,
+ Description: "CVE-2014-fake-4-description",
+ },
+ },
+ },
+ },
+ {
+ name: "match with normalization",
+ cpe: cpe.Must("cpe:2.3:*:ActiVERecord:ACTiveRecord:*:*:*:*:*:ruby:*:*", ""),
+ expected: []vulnerability.Vulnerability{
+ {
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-4",
+ Namespace: "nvd:cpe",
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
+ },
+ PackageQualifiers: []qualifier.Qualifier{},
+ Advisories: []vulnerability.Advisory{},
+ Metadata: &vulnerability.Metadata{
+ ID: "CVE-2014-fake-4",
+ DataSource: "http://somewhere/CVE-2014-fake-4",
+ Namespace: "nvd:cpe",
+ Severity: "High",
+ URLs: nil,
+ Description: "CVE-2014-fake-4-description",
+ },
+ },
+ },
+ },
+ {
+ name: "match from vendor & name",
+ cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:*:*:*", ""),
+ expected: []vulnerability.Vulnerability{
+ {
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""),
+ },
+ PackageQualifiers: []qualifier.Qualifier{},
+ Advisories: []vulnerability.Advisory{},
+ Metadata: &vulnerability.Metadata{
+ ID: "CVE-2014-fake-3",
+ DataSource: "http://somewhere/CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ Severity: "High",
+ URLs: nil,
+ Description: "CVE-2014-fake-3-description",
+ },
+ },
+ {
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-4",
+ Namespace: "nvd:cpe",
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
+ },
+ PackageQualifiers: []qualifier.Qualifier{},
+ Advisories: []vulnerability.Advisory{},
+ Metadata: &vulnerability.Metadata{
+ ID: "CVE-2014-fake-4",
+ DataSource: "http://somewhere/CVE-2014-fake-4",
+ Namespace: "nvd:cpe",
+ Severity: "High",
+ URLs: nil,
+ Description: "CVE-2014-fake-4-description",
+ },
+ },
+ },
+ },
+ {
+ name: "allow query with only product",
+ cpe: cpe.Must("cpe:2.3:a:*:product:*:*:*:*:*:*:*:*", ""),
+ },
+ {
+ name: "do not allow query without product",
+ cpe: cpe.CPE{
+ Attributes: cpe.Attributes{
+ Part: "a",
+ Vendor: "v",
+ },
+ },
+ err: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ provider := testVulnerabilityProvider(t)
+
+ actual, err := provider.FindVulnerabilities(search.ByCPE(test.cpe))
+ if err != nil && !test.err {
+ t.Fatalf("expected no err, got: %+v", err)
+ } else if err == nil && test.err {
+ t.Fatalf("expected an err, gots" +
+ " none")
+ }
+
+ require.Len(t, actual, len(test.expected))
+
+ for idx, vuln := range actual {
+ if d := cmp.Diff(test.expected[idx], vuln, cmpOpts()...); d != "" {
+ t.Errorf("diff: %+v", d)
+ }
+ }
+ })
+ }
+
+}
+
+func Test_FindVulnerabilitiesByByID(t *testing.T) {
+ provider := testVulnerabilityProvider(t)
+
+ d, err := distro.New(distro.Debian, "8", "")
+ require.NoError(t, err)
+
+ // with distro
+ actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByID("CVE-2014-fake-1"))
+ require.NoError(t, err)
+
+ expected := []vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-1",
+ Namespace: "debian:distro:debian:8",
+ },
+ PackageName: "neutron",
+ Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
+ PackageQualifiers: []qualifier.Qualifier{},
+ CPEs: nil,
+ Advisories: []vulnerability.Advisory{},
+ Metadata: &vulnerability.Metadata{
+ ID: "CVE-2014-fake-1",
+ DataSource: "http://somewhere/CVE-2014-fake-1",
+ Namespace: "debian:distro:debian:8",
+ Severity: "High",
+ URLs: nil,
+ Description: "CVE-2014-fake-1-description",
+ },
+ },
+ }
+
+ require.Len(t, actual, len(expected))
+
+ for idx, vuln := range actual {
+ if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" {
+ t.Errorf("diff: %+v", d)
+ }
+ }
+
+ // without distro
+ actual, err = provider.FindVulnerabilities(search.ByID("CVE-2014-fake-1"))
+ require.NoError(t, err)
+
+ for idx, vuln := range actual {
+ if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" {
+ t.Errorf("diff: %+v", d)
+ }
+ }
+
+ // prove we survive a bad request
+ actual, err = provider.FindVulnerabilities(search.ByDistro(*d), search.ByID("CVE-2014-fake-3"))
+ require.NoError(t, err)
+ 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
+ cmp.FilterPath(func(p cmp.Path) bool {
+ sf, ok := p.Index(-1).(cmp.StructField)
+ if !ok {
+ return false
+ }
+ r, _ := utf8.DecodeRuneInString(sf.Name())
+ return !unicode.IsUpper(r)
+ }, cmp.Ignore()),
+ cmpopts.EquateEmpty(),
+ cmpopts.IgnoreFields(vulnerability.Reference{}, "Internal"),
+ }
+}
diff --git a/grype/db/v6/vulnerability_store.go b/grype/db/v6/vulnerability_store.go
new file mode 100644
index 00000000000..471e228ba6b
--- /dev/null
+++ b/grype/db/v6/vulnerability_store.go
@@ -0,0 +1,393 @@
+package v6
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/scylladb/go-set/strset"
+ "gorm.io/gorm"
+
+ "github.com/anchore/go-logger"
+ "github.com/anchore/grype/internal/log"
+)
+
+const anyVulnerability = "any"
+
+type VulnerabilityStoreWriter interface {
+ AddVulnerabilities(vulns ...*VulnerabilityHandle) error
+}
+
+type VulnerabilityStoreReader interface {
+ GetVulnerabilities(vuln *VulnerabilitySpecifier, config *GetVulnerabilityOptions) ([]VulnerabilityHandle, error)
+}
+
+type GetVulnerabilityOptions struct {
+ Preload bool
+ Limit int
+}
+
+type VulnerabilitySpecifiers []VulnerabilitySpecifier
+
+type VulnerabilitySpecifier struct {
+ // Name of the vulnerability (e.g. CVE-2020-1234)
+ Name string
+
+ // ID is the DB ID of the vulnerability
+ ID ID
+
+ // Status is the status of the vulnerability (e.g. "active", "rejected", etc.)
+ Status VulnerabilityStatus
+
+ // PublishedAfter is a filter to only return vulnerabilities published after the given time
+ PublishedAfter *time.Time
+
+ // ModifiedAfter is a filter to only return vulnerabilities modified after the given time
+ ModifiedAfter *time.Time
+
+ // IncludeAliases for the given name or ID in results
+ IncludeAliases bool
+
+ // Providers
+ Providers []string
+}
+
+func (v *VulnerabilitySpecifier) String() string {
+ var parts []string
+ if v.Name != "" {
+ parts = append(parts, fmt.Sprintf("name=%s", v.Name))
+ }
+
+ if v.ID != 0 {
+ parts = append(parts, fmt.Sprintf("id=%d", v.ID))
+ }
+
+ if v.Status != "" {
+ parts = append(parts, fmt.Sprintf("status=%s", v.Status))
+ }
+
+ if v.PublishedAfter != nil {
+ parts = append(parts, fmt.Sprintf("publishedAfter=%s", v.PublishedAfter.String()))
+ }
+
+ if v.ModifiedAfter != nil {
+ parts = append(parts, fmt.Sprintf("modifiedAfter=%s", v.ModifiedAfter.String()))
+ }
+
+ if v.IncludeAliases {
+ parts = append(parts, "includeAliases=true")
+ }
+
+ if len(v.Providers) > 0 {
+ parts = append(parts, fmt.Sprintf("providers=%s", strings.Join(v.Providers, ",")))
+ }
+
+ if len(parts) == 0 {
+ return anyVulnerability
+ }
+
+ return fmt.Sprintf("vulnerability(%s)", strings.Join(parts, ", "))
+}
+
+func (s VulnerabilitySpecifiers) String() string {
+ if len(s) == 0 {
+ return anyVulnerability
+ }
+ var parts []string
+ for _, v := range s {
+ parts = append(parts, v.String())
+ }
+ return strings.Join(parts, ", ")
+}
+
+func DefaultGetVulnerabilityOptions() *GetVulnerabilityOptions {
+ return &GetVulnerabilityOptions{
+ Preload: false,
+ }
+}
+
+type vulnerabilityStore struct {
+ db *gorm.DB
+ blobStore *blobStore
+}
+
+func newVulnerabilityStore(db *gorm.DB, bs *blobStore) *vulnerabilityStore {
+ return &vulnerabilityStore{
+ db: db,
+ blobStore: bs,
+ }
+}
+
+func (s *vulnerabilityStore) AddVulnerabilities(vulnerabilities ...*VulnerabilityHandle) error {
+ if err := s.addProviders(s.db, vulnerabilities...); err != nil {
+ return fmt.Errorf("unable to add providers: %w", err)
+ }
+ for i := range vulnerabilities {
+ v := vulnerabilities[i]
+ // this adds the blob value to the DB and sets the ID on the vulnerability handle
+ if err := s.blobStore.addBlobable(v); err != nil {
+ return fmt.Errorf("unable to add affected blob: %w", err)
+ }
+
+ if v.PublishedDate != nil && v.ModifiedDate == nil {
+ // the data here should be consistent, and we are norming around initial publication counts as a modification date.
+ // this allows for easily refining queries based on both publication date and modification date without needing
+ // to worry about this edge case.
+ v.ModifiedDate = v.PublishedDate
+ }
+
+ if v.BlobValue != nil {
+ aliases := strset.New(v.BlobValue.Aliases...)
+ aliases.Remove(v.Name)
+ var aliasModels []VulnerabilityAlias
+ for _, alias := range aliases.List() {
+ aliasModels = append(aliasModels, VulnerabilityAlias{
+ Name: v.Name,
+ Alias: alias,
+ })
+ }
+ for _, aliasModel := range aliasModels {
+ if err := s.db.FirstOrCreate(&aliasModel).Error; err != nil {
+ return err
+ }
+ }
+ }
+ if err := createRecordsWithCache(s.db, v); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (s *vulnerabilityStore) addProviders(tx *gorm.DB, vulnerabilities ...*VulnerabilityHandle) error { // nolint:dupl
+ cacheInst, ok := cacheFromContext(tx.Statement.Context)
+ if !ok {
+ return fmt.Errorf("unable to fetch provider cache from context")
+ }
+
+ var final []*Provider
+ byCacheKey := make(map[string][]*Provider)
+ for _, v := range vulnerabilities {
+ if v.Provider != nil {
+ key := v.Provider.cacheKey()
+ if existingID, ok := cacheInst.getString(v.Provider); ok {
+ // seen in a previous transaction...
+ v.ProviderID = existingID
+ } else if _, ok := byCacheKey[key]; !ok {
+ // not seen within this transaction
+ final = append(final, v.Provider)
+ }
+ byCacheKey[key] = append(byCacheKey[key], v.Provider)
+ }
+ }
+
+ if len(final) == 0 {
+ return nil
+ }
+
+ if err := tx.Create(final).Error; err != nil {
+ return fmt.Errorf("unable to create provider records: %w", err)
+ }
+
+ // update the cache with the new records
+ for _, ref := range final {
+ cacheInst.set(ref)
+ }
+
+ // update all references with the IDs from the cache
+ for _, refs := range byCacheKey {
+ for _, ref := range refs {
+ id, ok := cacheInst.getString(ref)
+ if ok {
+ ref.setRowID(id)
+ }
+ }
+ }
+
+ // update the parent objects with the FK ID
+ for _, p := range vulnerabilities {
+ if p.Provider != nil {
+ p.ProviderID = p.Provider.ID
+ }
+ }
+ return nil
+}
+
+func createRecordsWithCache(tx *gorm.DB, items ...*VulnerabilityHandle) error {
+ // look for existing records from the cache, and only create new records
+ cacheInst, ok := cacheFromContext(tx.Statement.Context)
+ if !ok {
+ return fmt.Errorf("cache not found in context")
+ }
+
+ // store all entries by their cache key (throw away duplicates)
+ skippedRecordsByCacheKey := map[string][]*VulnerabilityHandle{}
+ usedKeys := strset.New()
+ var finalWrites []*VulnerabilityHandle
+ for i := range items {
+ p := items[i]
+ key := p.cacheKey()
+
+ if usedKeys.Has(key) {
+ skippedRecordsByCacheKey[key] = append(skippedRecordsByCacheKey[key], p)
+ continue
+ }
+
+ if _, ok := skippedRecordsByCacheKey[key]; ok {
+ skippedRecordsByCacheKey[key] = append(skippedRecordsByCacheKey[key], p)
+ continue
+ }
+ if _, ok := cacheInst.getID(p); ok {
+ skippedRecordsByCacheKey[key] = append(skippedRecordsByCacheKey[key], p)
+ continue
+ }
+
+ finalWrites = append(finalWrites, p)
+ usedKeys.Add(key)
+ }
+
+ for i := range finalWrites {
+ if err := tx.Omit("Provider").Create(finalWrites[i]).Error; err != nil {
+ return fmt.Errorf("unable to create record %#v: %w", finalWrites[i], err)
+ }
+ }
+
+ // ensure we're always updating the cache with the latest data + the records with any new IDs
+ for i := range finalWrites {
+ cacheInst.set(finalWrites[i])
+ }
+
+ for _, batch := range skippedRecordsByCacheKey {
+ for i := range batch {
+ id, ok := cacheInst.getID(batch[i])
+ if !ok {
+ return fmt.Errorf("unable to find ID: %#v", batch[i])
+ }
+ batch[i].setRowID(id)
+ }
+ }
+
+ return nil
+}
+
+func (s *vulnerabilityStore) GetVulnerabilities(vuln *VulnerabilitySpecifier, config *GetVulnerabilityOptions) ([]VulnerabilityHandle, error) {
+ if config == nil {
+ config = DefaultGetVulnerabilityOptions()
+ }
+ fields := logger.Fields{
+ "vuln": vuln,
+ "preload": config.Preload,
+ }
+ start := time.Now()
+ var count int
+ defer func() {
+ fields["duration"] = time.Since(start)
+ fields["records"] = count
+ log.WithFields(fields).Trace("fetched vulnerability records")
+ }()
+
+ var err error
+ query := s.db
+ if vuln != nil {
+ query, err = handleVulnerabilityOptions(s.db, query, *vuln)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ query = s.handlePreload(query, *config)
+
+ var models []VulnerabilityHandle
+
+ var results []*VulnerabilityHandle
+ if err := query.FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {
+ if config.Preload {
+ var blobs []blobable
+ for _, r := range results {
+ blobs = append(blobs, r)
+ }
+ if err := s.blobStore.attachBlobValue(blobs...); err != nil {
+ return fmt.Errorf("unable to attach vulnerability blobs: %w", err)
+ }
+ }
+
+ for _, r := range results {
+ models = append(models, *r)
+ }
+
+ count += len(results)
+
+ if config.Limit > 0 && len(models) >= config.Limit {
+ return ErrLimitReached
+ }
+
+ return nil
+ }).Error; err != nil {
+ return models, fmt.Errorf("unable to fetch vulnerability records: %w", err)
+ }
+
+ return models, err
+}
+
+func (s *vulnerabilityStore) handlePreload(query *gorm.DB, config GetVulnerabilityOptions) *gorm.DB {
+ var limitArgs []interface{}
+ if config.Limit > 0 {
+ query = query.Limit(config.Limit)
+ limitArgs = append(limitArgs, func(db *gorm.DB) *gorm.DB {
+ return db.Limit(config.Limit)
+ })
+ }
+ if config.Preload {
+ query = query.Preload("Provider", limitArgs...)
+ }
+ return query
+}
+
+func handleVulnerabilityOptions(base, parentQuery *gorm.DB, configs ...VulnerabilitySpecifier) (*gorm.DB, error) {
+ if len(configs) == 0 {
+ return parentQuery, nil
+ }
+
+ orConditions := base.Model(&VulnerabilityHandle{})
+ var includeAliasJoin bool
+ for _, config := range configs {
+ query := base.Model(&VulnerabilityHandle{})
+ if config.Name != "" {
+ if config.IncludeAliases {
+ includeAliasJoin = true
+ query = query.Where("vulnerability_handles.name = ? collate nocase OR vulnerability_aliases.alias = ? collate nocase", config.Name, config.Name)
+ } else {
+ query = query.Where("vulnerability_handles.name = ? collate nocase", config.Name)
+ }
+ }
+
+ if config.ID != 0 {
+ query = query.Where("vulnerability_handles.id = ?", config.ID)
+ }
+
+ if config.PublishedAfter != nil {
+ query = query.Where("vulnerability_handles.published_date > ?", *config.PublishedAfter)
+ }
+
+ if config.ModifiedAfter != nil {
+ query = query.Where("vulnerability_handles.modified_date > ?", *config.ModifiedAfter)
+ }
+
+ if config.Status != "" {
+ query = query.Where("vulnerability_handles.status = ?", config.Status)
+ }
+
+ if len(config.Providers) > 0 {
+ query = query.Where("vulnerability_handles.provider_id IN ?", config.Providers)
+ }
+
+ orConditions = orConditions.Or(query)
+ }
+
+ if includeAliasJoin {
+ parentQuery = parentQuery.Joins("LEFT JOIN vulnerability_aliases ON vulnerability_aliases.name = vulnerability_handles.name collate nocase")
+ }
+
+ return parentQuery.Where(orConditions), nil
+}
diff --git a/grype/db/v6/vulnerability_store_test.go b/grype/db/v6/vulnerability_store_test.go
new file mode 100644
index 00000000000..6c3cbc7a0ec
--- /dev/null
+++ b/grype/db/v6/vulnerability_store_test.go
@@ -0,0 +1,396 @@
+package v6
+
+import (
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestVulnerabilityStore_AddVulnerabilities(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newVulnerabilityStore(db, bw)
+
+ vuln1 := VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ BlobValue: &VulnerabilityBlob{
+ ID: "CVE-1234-5678",
+ },
+ Provider: &Provider{
+ ID: "provider!",
+ },
+ }
+
+ vuln2 := testVulnerabilityHandle()
+
+ err := s.AddVulnerabilities(&vuln1, &vuln2)
+ require.NoError(t, err)
+
+ var result1 VulnerabilityHandle
+ err = db.Where("name = ?", "CVE-1234-5678").First(&result1).Error
+ require.NoError(t, err)
+ assert.Equal(t, vuln1.Name, result1.Name)
+ assert.Equal(t, vuln1.ID, result1.ID)
+ assert.Equal(t, vuln1.BlobID, result1.BlobID)
+ assert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch
+ assert.Nil(t, result1.Provider) // since we're not preloading any fields on the fetch
+
+ var result2 VulnerabilityHandle
+ err = db.Where("name = ?", "CVE-8765-4321").First(&result2).Error
+ require.NoError(t, err)
+ assert.Equal(t, vuln2.Name, result2.Name)
+ assert.Equal(t, vuln2.ID, result2.ID)
+ assert.Equal(t, vuln2.BlobID, result2.BlobID)
+ assert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch
+ assert.Nil(t, result1.Provider) // since we're not preloading any fields on the fetch
+}
+
+func TestVulnerabilityStore_NoDuplicateVulnerabilities(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newVulnerabilityStore(db, bw)
+
+ vuln := VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ BlobValue: &VulnerabilityBlob{
+ ID: "CVE-1234-5678",
+ },
+ Provider: &Provider{
+ ID: "provider!",
+ },
+ }
+
+ err := s.AddVulnerabilities(&vuln)
+ require.NoError(t, err)
+
+ err = s.AddVulnerabilities(&vuln)
+ require.NoError(t, err)
+
+ var results []VulnerabilityHandle
+ err = db.Where("name = ?", "CVE-1234-5678").Preload("Provider").Find(&results).Error
+ require.NoError(t, err)
+ require.Len(t, results, 1, "expected exactly one vulnerability handle to be added")
+
+ result := results[0]
+ assert.NotEmpty(t, result.ProviderID)
+ assert.NotEmpty(t, result.BlobID)
+ if d := cmp.Diff(vuln, result, cmpopts.IgnoreFields(VulnerabilityHandle{}, "BlobValue")); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+}
+
+func TestVulnerabilityStore_AddVulnerabilities_missingModifiedDate(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newVulnerabilityStore(db, bw)
+
+ now := time.Now()
+ vuln := &VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ PublishedDate: &now, // have publication date without modification date
+ Provider: &Provider{
+ ID: "provider!",
+ },
+ }
+
+ err := s.AddVulnerabilities(vuln)
+ require.NoError(t, err)
+
+ // patched!
+ assert.NotNil(t, vuln.ModifiedDate)
+}
+
+func TestVulnerabilityStore_AddVulnerabilities_Aliases(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newVulnerabilityStore(db, bw)
+
+ vuln := &VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ BlobValue: &VulnerabilityBlob{
+ ID: "CVE-1234-5678",
+ Aliases: []string{"ALIAS-1", "ALIAS-2", "CVE-1234-5678"},
+ },
+ Provider: &Provider{
+ ID: "provider!",
+ },
+ }
+
+ err := s.AddVulnerabilities(vuln)
+ require.NoError(t, err)
+
+ var aliases []VulnerabilityAlias
+ err = db.Where("name = ?", "CVE-1234-5678").Find(&aliases).Error
+ require.NoError(t, err)
+
+ expectedAliases := []VulnerabilityAlias{
+ {Name: "CVE-1234-5678", Alias: "ALIAS-1"},
+ {Name: "CVE-1234-5678", Alias: "ALIAS-2"},
+ }
+ assert.Len(t, aliases, len(expectedAliases))
+
+ for _, expected := range expectedAliases {
+ assert.Contains(t, aliases, expected)
+ }
+
+ uniqueAliases := make(map[string]struct{})
+ for _, alias := range aliases {
+ key := alias.Name + ":" + alias.Alias
+ _, exists := uniqueAliases[key]
+ assert.False(t, exists, "duplicate alias found")
+ uniqueAliases[key] = struct{}{}
+ }
+}
+
+func TestVulnerabilityStore_GetVulnerability_ByID(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newVulnerabilityStore(db, bw)
+
+ vuln := testVulnerabilityHandle()
+ err := s.AddVulnerabilities(&vuln)
+ require.NoError(t, err)
+
+ results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{ID: vuln.ID}, nil) // don't preload by default
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+ result := results[0]
+
+ if d := cmp.Diff(vuln, result, cmpopts.IgnoreFields(VulnerabilityHandle{}, "Provider", "BlobValue")); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+ assert.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch
+ assert.Nil(t, result.Provider) // since we're not preloading any fields on the fetch
+
+ results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{ID: vuln.ID}, &GetVulnerabilityOptions{Preload: true})
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+ result = results[0]
+
+ assert.NotNil(t, result.BlobValue)
+ assert.NotNil(t, result.Provider)
+ if d := cmp.Diff(vuln, result); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+}
+
+func TestVulnerabilityStore_GetVulnerabilities_ByName(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newVulnerabilityStore(db, bw)
+
+ vuln1 := testVulnerabilityHandle()
+ name := vuln1.Name
+ vuln2 := VulnerabilityHandle{Name: name, BlobID: 2, Provider: vuln1.Provider, BlobValue: &VulnerabilityBlob{
+ ID: name,
+ }}
+ err := s.AddVulnerabilities(&vuln1, &vuln2)
+ require.NoError(t, err)
+
+ expected := []VulnerabilityHandle{vuln1, vuln2}
+
+ results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{Name: name}, nil) // don't preload by default
+ require.NoError(t, err)
+ require.Len(t, results, 2)
+ for i, result := range results {
+ assert.Equal(t, expected[i].Name, result.Name)
+ assert.Equal(t, expected[i].ID, result.ID)
+ assert.Equal(t, expected[i].BlobID, result.BlobID)
+ assert.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch
+ assert.Nil(t, result.Provider) // since we're not preloading any fields on the fetch
+ }
+
+ results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{Name: name}, &GetVulnerabilityOptions{Preload: true})
+ require.NoError(t, err)
+ require.Len(t, results, 2)
+
+ for i, result := range results {
+ if d := cmp.Diff(expected[i], result); d != "" {
+ t.Errorf("unexpected result (-want +got):\n%s", d)
+ }
+ }
+}
+
+func TestVulnerabilityStore_GetVulnerabilities_Aliases(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newVulnerabilityStore(db, bw)
+
+ vuln1 := &VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ BlobValue: &VulnerabilityBlob{
+ ID: "CVE-1234-5678",
+ Aliases: []string{"ALIAS-1", "ALIAS-2"},
+ },
+ Provider: &Provider{
+ ID: "provider!",
+ },
+ }
+
+ vuln2 := &VulnerabilityHandle{
+ Name: "ALIAS-1",
+ BlobValue: &VulnerabilityBlob{
+ ID: "ALIAS-1",
+ },
+ Provider: &Provider{
+ ID: "provider2!",
+ },
+ }
+
+ err := s.AddVulnerabilities(vuln1, vuln2)
+ require.NoError(t, err)
+
+ t.Run("include aliases", func(t *testing.T) {
+ specifierWithAliases := &VulnerabilitySpecifier{
+ Name: "ALIAS-1",
+ IncludeAliases: true,
+ }
+
+ results, err := s.GetVulnerabilities(specifierWithAliases, nil)
+ require.NoError(t, err)
+ require.Len(t, results, 2)
+ assert.ElementsMatch(t, []string{"CVE-1234-5678", "ALIAS-1"}, []string{results[0].Name, results[1].Name})
+ })
+
+ t.Run("dont include aliases", func(t *testing.T) {
+ specifierWithoutAliases := &VulnerabilitySpecifier{
+ Name: "ALIAS-1",
+ IncludeAliases: false,
+ }
+
+ results, err := s.GetVulnerabilities(specifierWithoutAliases, nil)
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+ assert.Equal(t, "ALIAS-1", results[0].Name)
+ })
+
+ t.Run("direct match without aliases", func(t *testing.T) {
+ specifierDirectMatch := &VulnerabilitySpecifier{
+ Name: "CVE-1234-5678",
+ IncludeAliases: false,
+ }
+
+ results, err := s.GetVulnerabilities(specifierDirectMatch, nil)
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+ assert.Equal(t, "CVE-1234-5678", results[0].Name)
+ })
+}
+
+func testVulnerabilityHandle() VulnerabilityHandle {
+ now := time.Now()
+
+ return VulnerabilityHandle{
+ Name: "CVE-8765-4321",
+ Status: "status!",
+ PublishedDate: &now,
+ ModifiedDate: &now,
+ WithdrawnDate: &now,
+ Provider: &Provider{
+ ID: "provider!",
+ },
+ BlobValue: &VulnerabilityBlob{
+ ID: "CVE-8765-4321",
+ Assigners: []string{"assigner!"},
+ Description: "description!",
+ References: []Reference{
+ {
+ URL: "url!",
+ Tags: []string{"tag!"},
+ },
+ },
+ Aliases: []string{"alias!"},
+ Severities: []Severity{
+ {
+ Scheme: "scheme!",
+ Value: "value!",
+ Source: "source!",
+ Rank: 10,
+ },
+ {
+ Scheme: SeveritySchemeCVSS,
+ Value: CVSSSeverity{
+ Vector: "CVSS:4.0/AV:L/AC:H/AT:P/PR:N/UI:P/VC:L/VI:H/VA:N/SC:N/SI:L/SA:N",
+ Version: "4.0",
+ },
+ },
+ },
+ },
+ }
+}
+
+func TestVulnerabilityStore_GetVulnerabilities_ByProviders(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newVulnerabilityStore(db, bw)
+
+ provider1 := &Provider{ID: "provider1"}
+ provider2 := &Provider{ID: "provider2"}
+
+ vuln1 := VulnerabilityHandle{Name: "CVE-1234-5678", BlobID: 1, Provider: provider1}
+ vuln2 := VulnerabilityHandle{Name: "CVE-2345-6789", BlobID: 2, Provider: provider2}
+
+ err := s.AddVulnerabilities(&vuln1, &vuln2)
+ require.NoError(t, err)
+
+ results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{"provider1"}}, nil)
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+ assert.Equal(t, vuln1.Name, results[0].Name)
+ assert.Equal(t, vuln1.Provider.ID, results[0].ProviderID)
+
+ results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{"provider1", "provider2"}}, nil)
+ require.NoError(t, err)
+ require.Len(t, results, 2)
+ assert.ElementsMatch(t, []string{vuln1.Name, vuln2.Name}, []string{results[0].Name, results[1].Name})
+}
+
+func TestVulnerabilityStore_GetVulnerabilities_FilterByMultipleFactors(t *testing.T) {
+ db := setupTestStore(t).db
+ bw := newBlobStore(db)
+ s := newVulnerabilityStore(db, bw)
+
+ now := time.Now()
+ oneDayAgo := now.Add(-24 * time.Hour)
+ halfDayAgo := now.Add(-12 * time.Hour)
+ tenDaysAgo := now.Add(-240 * time.Hour)
+
+ provider1 := &Provider{ID: "provider1"}
+ provider2 := &Provider{ID: "provider2"}
+
+ vuln1 := VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ BlobID: 1,
+ Provider: provider1,
+ PublishedDate: &halfDayAgo,
+ }
+
+ vuln2 := VulnerabilityHandle{
+ Name: "CVE-2345-6789",
+ BlobID: 2,
+ Provider: provider2, // filtered out due to provider
+ PublishedDate: &now,
+ }
+
+ vuln3 := VulnerabilityHandle{
+ Name: "CVE-1234-5678",
+ BlobID: 3,
+ Provider: provider1,
+ PublishedDate: &tenDaysAgo, // filtered out due to date
+ }
+
+ err := s.AddVulnerabilities(&vuln1, &vuln2, &vuln3)
+ require.NoError(t, err)
+
+ results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{
+ Providers: []string{"provider1"}, // filter by provider...
+ PublishedAfter: &oneDayAgo, // filter by date published...
+ }, nil)
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+ assert.Equal(t, vuln1.Name, results[0].Name)
+}
diff --git a/grype/db/v6/vulnerability_test.go b/grype/db/v6/vulnerability_test.go
new file mode 100644
index 00000000000..5335a3db626
--- /dev/null
+++ b/grype/db/v6/vulnerability_test.go
@@ -0,0 +1,589 @@
+package v6
+
+import (
+ "strings"
+ "testing"
+ "unicode"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+func TestV5Namespace(t *testing.T) {
+ // provider input should be derived from the Providers table:
+ // +------------+---------+---------------+----------------------------------+------------------------+
+ // | id | version | processor | date_captured | input_digest |
+ // +------------+---------+---------------+----------------------------------+------------------------+
+ // | nvd | 2 | vunnel@0.29.0 | 2025-01-08 01:32:55.179881+00:00 | xxh64:0a160d2b53dd0208 |
+ // | alpine | 1 | vunnel@0.29.0 | 2025-01-08 01:31:28.824872+00:00 | xxh64:30c5b7b8efa0c087 |
+ // | amazon | 1 | vunnel@0.29.0 | 2025-01-08 01:31:28.837469+00:00 | xxh64:7d90b3fa66b183bc |
+ // | chainguard | 1 | vunnel@0.29.0 | 2025-01-08 01:31:26.969865+00:00 | xxh64:25a82fa97ac9e077 |
+ // | debian | 1 | vunnel@0.29.0 | 2025-01-08 01:31:50.718966+00:00 | xxh64:4b1834b9e4e68987 |
+ // | github | 1 | vunnel@0.29.0 | 2025-01-08 01:31:27.450124+00:00 | xxh64:a3ee6b48d37a0124 |
+ // | mariner | 1 | vunnel@0.29.0 | 2025-01-08 01:32:35.005761+00:00 | xxh64:cb4f5861a1fda0af |
+ // | oracle | 1 | vunnel@0.29.0 | 2025-01-08 01:32:33.696274+00:00 | xxh64:72c0a15731e96ab3 |
+ // | rhel | 1 | vunnel@0.29.0 | 2025-01-08 01:32:32.192345+00:00 | xxh64:abf5d2fd5a26c194 |
+ // | sles | 1 | vunnel@0.29.0 | 2025-01-08 01:32:42.988937+00:00 | xxh64:8f558f8f28a04489 |
+ // | ubuntu | 3 | vunnel@0.29.0 | 2025-01-08 01:33:25.795537+00:00 | xxh64:97ef8421c0093620 |
+ // | wolfi | 1 | vunnel@0.29.0 | 2025-01-08 01:32:58.571417+00:00 | xxh64:f294f3474d35b1a9 |
+ // +------------+---------+---------------+----------------------------------+------------------------+
+
+ // the expected results should mimic what is found as v5 namespace values:
+ // +--------------------------------------+
+ // | namespace |
+ // +--------------------------------------+
+ // | nvd:cpe |
+ // | github:language:javascript |
+ // | ubuntu:distro:ubuntu:14.04 |
+ // | ubuntu:distro:ubuntu:16.04 |
+ // | ubuntu:distro:ubuntu:18.04 |
+ // | ubuntu:distro:ubuntu:20.04 |
+ // | ubuntu:distro:ubuntu:22.04 |
+ // | ubuntu:distro:ubuntu:22.10 |
+ // | ubuntu:distro:ubuntu:23.04 |
+ // | ubuntu:distro:ubuntu:23.10 |
+ // | ubuntu:distro:ubuntu:24.10 |
+ // | debian:distro:debian:8 |
+ // | debian:distro:debian:9 |
+ // | ubuntu:distro:ubuntu:12.04 |
+ // | ubuntu:distro:ubuntu:15.04 |
+ // | sles:distro:sles:15 |
+ // | sles:distro:sles:15.1 |
+ // | sles:distro:sles:15.2 |
+ // | sles:distro:sles:15.3 |
+ // | sles:distro:sles:15.4 |
+ // | sles:distro:sles:15.5 |
+ // | sles:distro:sles:15.6 |
+ // | amazon:distro:amazonlinux:2 |
+ // | debian:distro:debian:10 |
+ // | debian:distro:debian:11 |
+ // | debian:distro:debian:12 |
+ // | debian:distro:debian:unstable |
+ // | oracle:distro:oraclelinux:6 |
+ // | oracle:distro:oraclelinux:7 |
+ // | oracle:distro:oraclelinux:8 |
+ // | oracle:distro:oraclelinux:9 |
+ // | redhat:distro:redhat:6 |
+ // | redhat:distro:redhat:7 |
+ // | redhat:distro:redhat:8 |
+ // | redhat:distro:redhat:9 |
+ // | ubuntu:distro:ubuntu:12.10 |
+ // | ubuntu:distro:ubuntu:13.04 |
+ // | ubuntu:distro:ubuntu:14.10 |
+ // | ubuntu:distro:ubuntu:15.10 |
+ // | ubuntu:distro:ubuntu:16.10 |
+ // | ubuntu:distro:ubuntu:17.04 |
+ // | ubuntu:distro:ubuntu:17.10 |
+ // | ubuntu:distro:ubuntu:18.10 |
+ // | ubuntu:distro:ubuntu:19.04 |
+ // | ubuntu:distro:ubuntu:19.10 |
+ // | ubuntu:distro:ubuntu:20.10 |
+ // | ubuntu:distro:ubuntu:21.04 |
+ // | ubuntu:distro:ubuntu:21.10 |
+ // | ubuntu:distro:ubuntu:24.04 |
+ // | github:language:php |
+ // | debian:distro:debian:13 |
+ // | debian:distro:debian:7 |
+ // | redhat:distro:redhat:5 |
+ // | sles:distro:sles:11.1 |
+ // | sles:distro:sles:11.3 |
+ // | sles:distro:sles:11.4 |
+ // | sles:distro:sles:11.2 |
+ // | sles:distro:sles:12 |
+ // | sles:distro:sles:12.1 |
+ // | sles:distro:sles:12.2 |
+ // | sles:distro:sles:12.3 |
+ // | sles:distro:sles:12.4 |
+ // | sles:distro:sles:12.5 |
+ // | chainguard:distro:chainguard:rolling |
+ // | wolfi:distro:wolfi:rolling |
+ // | github:language:go |
+ // | alpine:distro:alpine:3.20 |
+ // | alpine:distro:alpine:3.21 |
+ // | alpine:distro:alpine:edge |
+ // | github:language:rust |
+ // | github:language:python |
+ // | sles:distro:sles:11 |
+ // | oracle:distro:oraclelinux:5 |
+ // | github:language:ruby |
+ // | github:language:dotnet |
+ // | alpine:distro:alpine:3.12 |
+ // | alpine:distro:alpine:3.13 |
+ // | alpine:distro:alpine:3.14 |
+ // | alpine:distro:alpine:3.15 |
+ // | alpine:distro:alpine:3.16 |
+ // | alpine:distro:alpine:3.17 |
+ // | alpine:distro:alpine:3.18 |
+ // | alpine:distro:alpine:3.19 |
+ // | mariner:distro:mariner:2.0 |
+ // | github:language:java |
+ // | github:language:dart |
+ // | amazon:distro:amazonlinux:2023 |
+ // | alpine:distro:alpine:3.10 |
+ // | alpine:distro:alpine:3.11 |
+ // | alpine:distro:alpine:3.4 |
+ // | alpine:distro:alpine:3.5 |
+ // | alpine:distro:alpine:3.7 |
+ // | alpine:distro:alpine:3.8 |
+ // | alpine:distro:alpine:3.9 |
+ // | mariner:distro:azurelinux:3.0 |
+ // | mariner:distro:mariner:1.0 |
+ // | alpine:distro:alpine:3.3 |
+ // | alpine:distro:alpine:3.6 |
+ // | amazon:distro:amazonlinux:2022 |
+ // | alpine:distro:alpine:3.2 |
+ // | github:language:swift |
+ // +--------------------------------------+
+
+ type testCase struct {
+ 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{
+ // NVD
+ {
+ name: "nvd provider",
+ provider: "nvd",
+ expected: "nvd:cpe",
+ },
+
+ // GitHub ecosystem tests
+ {
+ name: "github golang direct",
+ provider: "github",
+ ecosystem: "golang",
+ expected: "github:language:go",
+ },
+ {
+ name: "github go-module ecosystem",
+ provider: "github",
+ ecosystem: "go-module",
+ expected: "github:language:go",
+ },
+ {
+ name: "github composer ecosystem",
+ provider: "github",
+ ecosystem: "composer",
+ expected: "github:language:php",
+ },
+ {
+ name: "github php-composer ecosystem",
+ provider: "github",
+ ecosystem: "php-composer",
+ expected: "github:language:php",
+ },
+ {
+ name: "github cargo ecosystem",
+ provider: "github",
+ ecosystem: "cargo",
+ expected: "github:language:rust",
+ },
+ {
+ name: "github rust-crate ecosystem",
+ provider: "github",
+ ecosystem: "rust-crate",
+ expected: "github:language:rust",
+ },
+ {
+ name: "github pub ecosystem",
+ provider: "github",
+ ecosystem: "pub",
+ expected: "github:language:dart",
+ },
+ {
+ name: "github dart-pub ecosystem",
+ provider: "github",
+ ecosystem: "dart-pub",
+ expected: "github:language:dart",
+ },
+ {
+ name: "github nuget ecosystem",
+ provider: "github",
+ ecosystem: "nuget",
+ expected: "github:language:dotnet",
+ },
+ {
+ name: "github maven ecosystem",
+ provider: "github",
+ ecosystem: "maven",
+ expected: "github:language:java",
+ },
+ {
+ name: "github java ecosystem",
+ provider: "github",
+ ecosystem: "java",
+ expected: "github:language:java",
+ },
+ {
+ name: "syft pkg type java-archive",
+ provider: "github",
+ ecosystem: "java-archive",
+ expected: "github:language:java",
+ },
+ {
+ name: "github swifturl ecosystem",
+ provider: "github",
+ ecosystem: "swifturl",
+ expected: "github:language:swift",
+ },
+ {
+ name: "github npm ecosystem",
+ provider: "github",
+ ecosystem: "npm",
+ expected: "github:language:javascript",
+ },
+ {
+ name: "github node ecosystem",
+ provider: "github",
+ ecosystem: "node",
+ expected: "github:language:javascript",
+ },
+ {
+ name: "github pypi ecosystem",
+ provider: "github",
+ ecosystem: "pypi",
+ expected: "github:language:python",
+ },
+ {
+ name: "github pip ecosystem",
+ provider: "github",
+ ecosystem: "pip",
+ expected: "github:language:python",
+ },
+ {
+ name: "github rubygems ecosystem",
+ provider: "github",
+ ecosystem: "rubygems",
+ expected: "github:language:ruby",
+ },
+ {
+ name: "github gem ecosystem",
+ provider: "github",
+ ecosystem: "gem",
+ expected: "github:language:ruby",
+ },
+
+ // OS Distribution tests
+ {
+ name: "ubuntu distribution",
+ provider: "ubuntu",
+ osName: "ubuntu",
+ osVersion: "22.04",
+ expected: "ubuntu:distro:ubuntu:22.04",
+ },
+ {
+ name: "ubuntu distribution (trimmed 0s)",
+ provider: "ubuntu",
+ osName: "ubuntu",
+ osVersion: "22.4",
+ expected: "ubuntu:distro:ubuntu:22.04",
+ },
+ {
+ name: "redhat distribution",
+ provider: "rhel",
+ osName: "redhat",
+ osVersion: "8",
+ expected: "redhat:distro:redhat:8",
+ },
+ {
+ name: "debian distribution",
+ provider: "debian",
+ osName: "debian",
+ osVersion: "11",
+ expected: "debian:distro:debian:11",
+ },
+ {
+ name: "sles distribution",
+ provider: "sles",
+ osName: "sles",
+ osVersion: "15.5",
+ expected: "sles:distro:sles:15.5",
+ },
+ {
+ name: "alpine distribution",
+ provider: "alpine",
+ osName: "alpine",
+ osVersion: "3.18",
+ expected: "alpine:distro:alpine:3.18",
+ },
+ {
+ name: "chainguard distribution",
+ provider: "chainguard",
+ osName: "chainguard",
+ osVersion: "rolling",
+ expected: "chainguard:distro:chainguard:rolling",
+ },
+ {
+ name: "wolfi distribution",
+ provider: "wolfi",
+ osName: "wolfi",
+ osVersion: "rolling",
+ expected: "wolfi:distro:wolfi:rolling",
+ },
+ {
+ name: "amazon linux distribution",
+ provider: "amazon",
+ osName: "amazon",
+ osVersion: "2023",
+ expected: "amazon:distro:amazonlinux:2023",
+ },
+ {
+ name: "mariner regular version",
+ provider: "mariner",
+ osName: "mariner",
+ osVersion: "2.0",
+ expected: "mariner:distro:mariner:2.0",
+ },
+ {
+ name: "mariner regular version (not exact match)",
+ provider: "mariner",
+ osName: "mariner",
+ osVersion: "2.1",
+ expected: "mariner:distro:mariner:2.1",
+ },
+ {
+ name: "mariner regular version (auto fill minor version)",
+ provider: "mariner",
+ osName: "mariner",
+ osVersion: "1",
+ expected: "mariner:distro:mariner:1.0",
+ },
+ {
+ name: "mariner azure version",
+ provider: "mariner",
+ osName: "mariner",
+ osVersion: "3.0",
+ expected: "mariner:distro:azurelinux:3.0",
+ },
+ {
+ name: "mariner azure version (missing version fields)",
+ provider: "mariner",
+ osName: "mariner",
+ osVersion: "3",
+ expected: "mariner:distro:azurelinux:3.0",
+ },
+ {
+ name: "azurelinux version (extra version fields)",
+ provider: "mariner",
+ osName: "azurelinux",
+ osVersion: "3.0.20240727",
+ expected: "mariner:distro:azurelinux:3.0",
+ },
+ {
+ name: "azurelinux version",
+ provider: "mariner",
+ osName: "azurelinux",
+ osVersion: "3.0",
+ expected: "mariner:distro:azurelinux:3.0",
+ },
+ {
+ name: "azurelinux version (missing version fields)",
+ provider: "mariner",
+ osName: "azurelinux",
+ osVersion: "3",
+ expected: "mariner:distro:azurelinux:3.0",
+ },
+ {
+ name: "mariner azure version (extra version fields)",
+ provider: "mariner",
+ osName: "mariner",
+ osVersion: "3.0.20240727",
+ expected: "mariner:distro:azurelinux:3.0",
+ },
+ {
+ name: "oracle linux distribution",
+ provider: "oracle",
+ osName: "oracle",
+ osVersion: "8",
+ expected: "oracle:distro:oraclelinux:8",
+ },
+
+ // Version truncation tests
+ {
+ name: "rhel with minor version",
+ provider: "rhel",
+ osName: "redhat",
+ osVersion: "8.6",
+ expected: "redhat:distro:redhat:8",
+ },
+ {
+ name: "rhel with patch version",
+ provider: "rhel",
+ osName: "redhat",
+ osVersion: "9.2.1",
+ expected: "redhat:distro:redhat:9",
+ },
+ {
+ name: "oracle with minor version",
+ provider: "oracle",
+ osName: "oracle",
+ osVersion: "8.7",
+ expected: "oracle:distro:oraclelinux:8",
+ },
+ {
+ name: "oracle with patch version",
+ provider: "oracle",
+ osName: "oracle",
+ 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 {
+ t.Run(tt.name, func(t *testing.T) {
+ vuln := &VulnerabilityHandle{
+ Provider: &Provider{
+ ID: tt.provider,
+ },
+ }
+
+ pkg := &AffectedPackageHandle{}
+
+ if tt.osName != "" {
+ major, minor, _ := majorMinorPatch(tt.osVersion)
+ var label string
+ if major == "" {
+ label = tt.osVersion
+ }
+ pkg.OperatingSystem = &OperatingSystem{
+ Name: tt.osName,
+ MajorVersion: major,
+ MinorVersion: minor,
+ LabelVersion: label,
+ }
+ pkg.Package = &Package{
+ Name: "os-package",
+ Ecosystem: "os-ecosystem",
+ }
+ } else if tt.ecosystem != "" {
+ pkg.Package = &Package{
+ Ecosystem: tt.ecosystem,
+ Name: tt.packageName,
+ }
+ }
+
+ result := MimicV5Namespace(vuln, pkg)
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+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 "", "", ""
+ }
+ parts := strings.Split(ver, ".")
+ if len(parts) == 0 {
+ return "", "", ""
+ }
+ if len(parts) == 1 {
+ return parts[0], "", ""
+ }
+ if len(parts) == 2 {
+ return parts[0], parts[1], ""
+ }
+ return parts[0], parts[1], parts[2]
+}
diff --git a/grype/db/vulnerability_metadata_provider.go b/grype/db/vulnerability_metadata_provider.go
deleted file mode 100644
index 4e5347e185c..00000000000
--- a/grype/db/vulnerability_metadata_provider.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package db
-
-import (
- "fmt"
-
- grypeDB "github.com/anchore/grype/grype/db/v5"
- "github.com/anchore/grype/grype/vulnerability"
-)
-
-var _ vulnerability.MetadataProvider = (*VulnerabilityMetadataProvider)(nil)
-
-type VulnerabilityMetadataProvider struct {
- reader grypeDB.VulnerabilityMetadataStoreReader
-}
-
-func NewVulnerabilityMetadataProvider(reader grypeDB.VulnerabilityMetadataStoreReader) *VulnerabilityMetadataProvider {
- return &VulnerabilityMetadataProvider{
- reader: reader,
- }
-}
-
-func (pr *VulnerabilityMetadataProvider) GetMetadata(id, namespace string) (*vulnerability.Metadata, error) {
- metadata, err := pr.reader.GetVulnerabilityMetadata(id, namespace)
- if err != nil {
- return nil, fmt.Errorf("metadata provider failed to fetch id='%s' recordsource='%s': %w", id, namespace, err)
- }
-
- return vulnerability.NewMetadata(metadata)
-}
diff --git a/grype/db/vulnerability_provider.go b/grype/db/vulnerability_provider.go
deleted file mode 100644
index 0b9d7959745..00000000000
--- a/grype/db/vulnerability_provider.go
+++ /dev/null
@@ -1,183 +0,0 @@
-package db
-
-import (
- "fmt"
-
- "github.com/facebookincubator/nvdtools/wfn"
-
- cpeUtil "github.com/anchore/grype/grype/cpe"
- grypeDB "github.com/anchore/grype/grype/db/v5"
- "github.com/anchore/grype/grype/db/v5/namespace"
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/grype/internal/log"
- "github.com/anchore/syft/syft/cpe"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-var _ vulnerability.Provider = (*VulnerabilityProvider)(nil)
-
-type VulnerabilityProvider struct {
- namespaceIndex *namespace.Index
- reader grypeDB.VulnerabilityStoreReader
-}
-
-func NewVulnerabilityProvider(reader grypeDB.VulnerabilityStoreReader) (*VulnerabilityProvider, error) {
- namespaces, err := reader.GetVulnerabilityNamespaces()
- if err != nil {
- return nil, fmt.Errorf("unable to get namespaces from store: %w", err)
- }
-
- namespaceIndex, err := namespace.FromStrings(namespaces)
- if err != nil {
- return nil, fmt.Errorf("unable to parse namespaces from store: %w", err)
- }
-
- return &VulnerabilityProvider{
- namespaceIndex: namespaceIndex,
- reader: reader,
- }, nil
-}
-
-func (pr *VulnerabilityProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
- // note: getting a vulnerability record by id doesn't necessarily return a single record
- // since records are duplicated by the set of fixes they have.
- vulns, err := pr.reader.GetVulnerability(namespace, id)
- if err != nil {
- return nil, fmt.Errorf("provider failed to fetch namespace=%q pkg=%q: %w", namespace, id, err)
- }
-
- var results []vulnerability.Vulnerability
- for _, vuln := range vulns {
- vulnObj, err := vulnerability.NewVulnerability(vuln)
- if err != nil {
- return nil, fmt.Errorf("provider failed to inflate vulnerability record (namespace=%q id=%q): %w", vuln.Namespace, vuln.ID, err)
- }
-
- results = append(results, *vulnObj)
- }
- return results, nil
-}
-
-func (pr *VulnerabilityProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- if d == nil {
- return nil, nil
- }
-
- var vulnerabilities []vulnerability.Vulnerability
- namespaces := pr.namespaceIndex.NamespacesForDistro(d)
-
- if len(namespaces) == 0 {
- log.Debugf("no vulnerability namespaces found in grype database for distro=%s package=%s", d.String(), p.Name)
- return vulnerabilities, nil
- }
-
- vulnerabilities = make([]vulnerability.Vulnerability, 0)
-
- for _, n := range namespaces {
- for _, packageName := range n.Resolver().Resolve(p) {
- nsStr := n.String()
- allPkgVulns, err := pr.reader.SearchForVulnerabilities(nsStr, packageName)
-
- if err != nil {
- return nil, fmt.Errorf("provider failed to search for vulnerabilities (namespace=%q pkg=%q): %w", nsStr, packageName, err)
- }
-
- for _, vuln := range allPkgVulns {
- vulnObj, err := vulnerability.NewVulnerability(vuln)
- if err != nil {
- return nil, fmt.Errorf("provider failed to inflate vulnerability record (namespace=%q id=%q distro=%q): %w", vuln.Namespace, vuln.ID, d, err)
- }
-
- vulnerabilities = append(vulnerabilities, *vulnObj)
- }
- }
- }
-
- return vulnerabilities, nil
-}
-
-func (pr *VulnerabilityProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- var vulnerabilities []vulnerability.Vulnerability
- namespaces := pr.namespaceIndex.NamespacesForLanguage(l)
-
- if len(namespaces) == 0 {
- log.Debugf("no vulnerability namespaces found in grype database for language=%s package=%s", l, p.Name)
- return vulnerabilities, nil
- }
-
- vulnerabilities = make([]vulnerability.Vulnerability, 0)
-
- for _, n := range namespaces {
- for _, packageName := range n.Resolver().Resolve(p) {
- nsStr := n.String()
- allPkgVulns, err := pr.reader.SearchForVulnerabilities(nsStr, packageName)
-
- if err != nil {
- return nil, fmt.Errorf("provider failed to fetch namespace=%q pkg=%q: %w", nsStr, packageName, err)
- }
-
- for _, vuln := range allPkgVulns {
- vulnObj, err := vulnerability.NewVulnerability(vuln)
- if err != nil {
- return nil, fmt.Errorf("provider failed to inflate vulnerability record (namespace=%q id=%q language=%q): %w", vuln.Namespace, vuln.ID, l, err)
- }
-
- vulnerabilities = append(vulnerabilities, *vulnObj)
- }
- }
- }
-
- return vulnerabilities, nil
-}
-
-func (pr *VulnerabilityProvider) GetByCPE(requestCPE cpe.CPE) ([]vulnerability.Vulnerability, error) {
- vulns := make([]vulnerability.Vulnerability, 0)
- namespaces := pr.namespaceIndex.CPENamespaces()
-
- if len(namespaces) == 0 {
- log.Debugf("no vulnerability namespaces found for arbitrary CPEs in grype database")
- return nil, nil
- }
-
- if requestCPE.Product == wfn.Any || requestCPE.Product == wfn.NA {
- return nil, fmt.Errorf("product name is required")
- }
-
- for _, ns := range namespaces {
- allPkgVulns, err := pr.reader.SearchForVulnerabilities(ns.String(), ns.Resolver().Normalize(requestCPE.Product))
- if err != nil {
- return nil, fmt.Errorf("provider failed to fetch namespace=%q product=%q: %w", ns, requestCPE.Product, err)
- }
-
- normalizedRequestCPE, err := cpe.New(ns.Resolver().Normalize(requestCPE.BindToFmtString()))
-
- if err != nil {
- normalizedRequestCPE = requestCPE
- }
-
- for _, vuln := range allPkgVulns {
- vulnCPEs, err := cpeUtil.NewSlice(vuln.CPEs...)
- if err != nil {
- return nil, err
- }
-
- // compare the request CPE to the potential matches (excluding version, which is handled downstream)
- candidateMatchCpes := cpeUtil.MatchWithoutVersion(normalizedRequestCPE, vulnCPEs)
-
- if len(candidateMatchCpes) > 0 {
- vulnObj, err := vulnerability.NewVulnerability(vuln)
- if err != nil {
- return nil, fmt.Errorf("provider failed to inflate vulnerability record (namespace=%q id=%q cpe=%q): %w", vuln.Namespace, vuln.ID, requestCPE.BindToFmtString(), err)
- }
-
- vulnObj.CPEs = candidateMatchCpes
-
- vulns = append(vulns, *vulnObj)
- }
- }
- }
-
- return vulns, nil
-}
diff --git a/grype/db/vulnerability_provider_mocks_test.go b/grype/db/vulnerability_provider_mocks_test.go
deleted file mode 100644
index 06db793b469..00000000000
--- a/grype/db/vulnerability_provider_mocks_test.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package db
-
-import grypeDB "github.com/anchore/grype/grype/db/v5"
-
-type mockStore struct {
- data map[string]map[string][]grypeDB.Vulnerability
-}
-
-func newMockStore() *mockStore {
- d := mockStore{
- data: make(map[string]map[string][]grypeDB.Vulnerability),
- }
- d.stub()
- return &d
-}
-
-func (d *mockStore) stub() {
- d.data["debian:distro:debian:8"] = map[string][]grypeDB.Vulnerability{
- "neutron": {
- {
- PackageName: "neutron",
- Namespace: "debian:distro:debian:8",
- VersionConstraint: "< 2014.1.3-6",
- ID: "CVE-2014-fake-1",
- VersionFormat: "deb",
- },
- {
- PackageName: "neutron",
- Namespace: "debian:distro:debian:8",
- VersionConstraint: "< 2013.0.2-1",
- ID: "CVE-2013-fake-2",
- VersionFormat: "deb",
- },
- },
- }
- d.data["nvd:cpe"] = map[string][]grypeDB.Vulnerability{
- "activerecord": {
- {
- PackageName: "activerecord",
- Namespace: "nvd:cpe",
- VersionConstraint: "< 3.7.6",
- ID: "CVE-2014-fake-3",
- VersionFormat: "unknown",
- CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
- },
- },
- {
- PackageName: "activerecord",
- Namespace: "nvd:cpe",
- VersionConstraint: "< 3.7.4",
- ID: "CVE-2014-fake-4",
- VersionFormat: "unknown",
- CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*",
- },
- },
- {
- PackageName: "activerecord",
- Namespace: "nvd:cpe",
- VersionConstraint: "= 4.0.1",
- ID: "CVE-2014-fake-5",
- VersionFormat: "unknown",
- CPEs: []string{
- "cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*",
- },
- },
- {
- PackageName: "activerecord",
- Namespace: "nvd:cpe",
- VersionConstraint: "< 98SP3",
- ID: "CVE-2014-fake-6",
- VersionFormat: "unknown",
- CPEs: []string{
- "cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*",
- },
- },
- },
- }
-}
-
-func (d *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
- var results []grypeDB.Vulnerability
- for _, vulns := range d.data[namespace] {
- for _, vuln := range vulns {
- if vuln.ID == id {
- results = append(results, vuln)
- }
- }
- }
- return results, nil
-}
-
-func (d *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
- return d.data[namespace][name], nil
-}
-
-func (d *mockStore) GetAllVulnerabilities() (*[]grypeDB.Vulnerability, error) {
- return nil, nil
-}
-
-func (d *mockStore) GetVulnerabilityNamespaces() ([]string, error) {
- keys := make([]string, 0, len(d.data))
- for k := range d.data {
- keys = append(keys, k)
- }
-
- return keys, nil
-}
diff --git a/grype/db/vulnerability_provider_test.go b/grype/db/vulnerability_provider_test.go
deleted file mode 100644
index bbd946a309b..00000000000
--- a/grype/db/vulnerability_provider_test.go
+++ /dev/null
@@ -1,201 +0,0 @@
-package db
-
-import (
- "testing"
-
- "github.com/go-test/deep"
- "github.com/google/uuid"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/pkg/qualifier"
- "github.com/anchore/grype/grype/version"
- "github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/syft/syft/cpe"
-)
-
-func Test_GetByDistro(t *testing.T) {
- provider, err := NewVulnerabilityProvider(newMockStore())
- require.NoError(t, err)
-
- d, err := distro.New(distro.Debian, "8", "")
- require.NoError(t, err)
-
- p := pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "neutron",
- }
-
- actual, err := provider.GetByDistro(d, p)
- require.NoError(t, err)
-
- expected := []vulnerability.Vulnerability{
- {
- Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
- ID: "CVE-2014-fake-1",
- Namespace: "debian:distro:debian:8",
- PackageQualifiers: []qualifier.Qualifier{},
- CPEs: []cpe.CPE{},
- Advisories: []vulnerability.Advisory{},
- },
- {
- Constraint: version.MustGetConstraint("< 2013.0.2-1", version.DebFormat),
- ID: "CVE-2013-fake-2",
- Namespace: "debian:distro:debian:8",
- PackageQualifiers: []qualifier.Qualifier{},
- CPEs: []cpe.CPE{},
- Advisories: []vulnerability.Advisory{},
- },
- }
-
- assert.Len(t, actual, len(expected))
-
- for idx, vuln := range actual {
- for _, d := range deep.Equal(expected[idx], vuln) {
- t.Errorf("diff: %+v", d)
- }
- }
-}
-
-func Test_GetByDistro_nilDistro(t *testing.T) {
- provider, err := NewVulnerabilityProvider(newMockStore())
- require.NoError(t, err)
-
- p := pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "neutron",
- }
-
- vulnerabilities, err := provider.GetByDistro(nil, p)
-
- assert.Empty(t, vulnerabilities)
- assert.NoError(t, err)
-}
-
-func Test_GetByCPE(t *testing.T) {
-
- tests := []struct {
- name string
- cpe cpe.CPE
- expected []vulnerability.Vulnerability
- err bool
- }{
- {
- name: "match from name and target SW",
- cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"),
- expected: []vulnerability.Vulnerability{
- {
- Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
- ID: "CVE-2014-fake-4",
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*"),
- },
- Namespace: "nvd:cpe",
- PackageQualifiers: []qualifier.Qualifier{},
- Advisories: []vulnerability.Advisory{},
- },
- },
- },
- {
- name: "match with normalization",
- cpe: cpe.Must("cpe:2.3:*:ActiVERecord:ACTiveRecord:*:*:*:*:*:ruby:*:*"),
- expected: []vulnerability.Vulnerability{
- {
- Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
- ID: "CVE-2014-fake-4",
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*"),
- },
- Namespace: "nvd:cpe",
- PackageQualifiers: []qualifier.Qualifier{},
- Advisories: []vulnerability.Advisory{},
- },
- },
- },
- {
- name: "match from vendor & name",
- cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:*:*:*"),
- expected: []vulnerability.Vulnerability{
- {
- Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
- ID: "CVE-2014-fake-3",
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
- },
- Namespace: "nvd:cpe",
- PackageQualifiers: []qualifier.Qualifier{},
- Advisories: []vulnerability.Advisory{},
- },
- {
- Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
- ID: "CVE-2014-fake-4",
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*"),
- },
- Namespace: "nvd:cpe",
- PackageQualifiers: []qualifier.Qualifier{},
- Advisories: []vulnerability.Advisory{},
- },
- },
- },
-
- {
- name: "dont allow any name",
- cpe: cpe.Must("cpe:2.3:*:couldntgetthisrightcouldyou:*:*:*:*:*:*:*:*:*"),
- err: true,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
-
- provider, err := NewVulnerabilityProvider(newMockStore())
- require.NoError(t, err)
-
- actual, err := provider.GetByCPE(test.cpe)
- if err != nil && !test.err {
- t.Fatalf("expected no err, got: %+v", err)
- } else if err == nil && test.err {
- t.Fatalf("expected an err, got none")
- }
-
- assert.Len(t, actual, len(test.expected))
-
- for idx, vuln := range actual {
- for _, d := range deep.Equal(test.expected[idx], vuln) {
- t.Errorf("diff: %+v", d)
- }
- }
- })
- }
-
-}
-
-func Test_Get(t *testing.T) {
- provider, err := NewVulnerabilityProvider(newMockStore())
- require.NoError(t, err)
-
- actual, err := provider.Get("CVE-2014-fake-1", "debian:distro:debian:8")
- require.NoError(t, err)
-
- expected := []vulnerability.Vulnerability{
- {
- Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
- ID: "CVE-2014-fake-1",
- Namespace: "debian:distro:debian:8",
- PackageQualifiers: []qualifier.Qualifier{},
- CPEs: []cpe.CPE{},
- Advisories: []vulnerability.Advisory{},
- },
- }
-
- require.Len(t, actual, len(expected))
-
- for idx, vuln := range actual {
- for _, d := range deep.Equal(expected[idx], vuln) {
- t.Errorf("diff: %+v", d)
- }
- }
-}
diff --git a/grype/deprecated.go b/grype/deprecated.go
index f67e1206355..050974fef36 100644
--- a/grype/deprecated.go
+++ b/grype/deprecated.go
@@ -1,25 +1,26 @@
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"
- "github.com/anchore/grype/grype/store"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
"github.com/anchore/stereoscope/pkg/image"
- "github.com/anchore/syft/syft/linux"
- "github.com/anchore/syft/syft/pkg/cataloger"
+ "github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/source"
)
-// TODO: deprecated, remove in v1.0.0
-func FindVulnerabilities(store store.Store, userImageStr string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) (match.Matches, pkg.Context, []pkg.Package, error) {
+// TODO: deprecated, will remove before v1.0.0
+func FindVulnerabilities(store vulnerability.Provider, userImageStr string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) (match.Matches, pkg.Context, []pkg.Package, error) {
providerConfig := pkg.ProviderConfig{
SyftProviderConfig: pkg.SyftProviderConfig{
- RegistryOptions: registryOptions,
- CatalogingOptions: cataloger.DefaultConfig(),
+ RegistryOptions: registryOptions,
+ SBOMOptions: syft.DefaultCreateSBOMConfig(),
},
}
- providerConfig.CatalogingOptions.Search.Scope = scopeOpt
+ providerConfig.SBOMOptions.Search.Scope = scopeOpt
packages, context, _, err := pkg.Provide(userImageStr, providerConfig)
if err != nil {
@@ -31,7 +32,22 @@ func FindVulnerabilities(store store.Store, userImageStr string, scopeOpt source
return FindVulnerabilitiesForPackage(store, context.Distro, matchers, packages), context, packages, nil
}
-// TODO: deprecated, remove in v1.0.0
-func FindVulnerabilitiesForPackage(store store.Store, d *linux.Release, matchers []matcher.Matcher, packages []pkg.Package) match.Matches {
- return matcher.FindMatches(store, d, matchers, packages)
+// TODO: deprecated, will remove before v1.0.0
+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,
+ ExclusionProvider: exclusionProvider,
+ Matchers: matchers,
+ NormalizeByCVE: false,
+ }
+
+ actualResults, _, err := runner.FindMatches(packages, pkg.Context{
+ Distro: d,
+ })
+ if err != nil || actualResults == nil {
+ log.WithFields("error", err).Error("unable to find vulnerabilities")
+ return match.NewMatches()
+ }
+ return *actualResults
}
diff --git a/grype/distro/distro.go b/grype/distro/distro.go
index b0315cc33f1..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,13 +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
}
}
- return New(t, selectedVersion, release.IDLike...)
+ if selectedVersion == "" {
+ selectedVersion = release.VersionID
+ }
+
+ return New(t, selectedVersion, release.VersionCodename, release.IDLike...)
}
func (d Distro) Name() string {
@@ -66,34 +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 {
+ 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 c757119c53f..c9ca73e6972 100644
--- a/grype/distro/distro_test.go
+++ b/grype/distro/distro_test.go
@@ -3,22 +3,25 @@ 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"
"github.com/anchore/grype/internal/stringutil"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/source"
+ "github.com/anchore/syft/syft/source/directorysource"
)
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",
@@ -26,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",
@@ -37,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",
@@ -47,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",
@@ -64,170 +81,221 @@ func Test_NewDistroFromRelease(t *testing.T) {
ID: "bogosity",
VersionID: "8",
},
- expectErr: true,
+ expectErr: require.Error,
+ },
+ {
+ // syft -o json debian:testing | jq .distro
+ name: "unstable debian",
+ release: linux.Release{
+ ID: "debian",
+ VersionID: "",
+ Version: "",
+ PrettyName: "Debian GNU/Linux trixie/sid",
+ VersionCodename: "trixie",
+ Name: "Debian GNU/Linux",
+ },
+ expected: &Distro{
+ Type: Debian,
+ Codename: "trixie",
+ },
+ major: "",
+ minor: "",
+ },
+ {
+ name: "azure linux 3",
+ release: linux.Release{
+ ID: "azurelinux",
+ Version: "3.0.20240417",
+ VersionID: "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",
+ },
+ {
+ Name: "test-fixtures/os/debian-sid",
+ Type: Debian,
+ LabelVersion: "trixie",
},
{
- fixture: "test-fixtures/os/fedora",
+ 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",
+ },
+ {
+ Name: "test-fixtures/os/azurelinux",
+ Type: Azure,
+ 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 := source.NewFromDirectory(source.DirectoryConfig{Path: 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)
@@ -243,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")
})
}
@@ -293,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/azurelinux/etc/os-release b/grype/distro/test-fixtures/os/azurelinux/etc/os-release
new file mode 100644
index 00000000000..b7352fb601a
--- /dev/null
+++ b/grype/distro/test-fixtures/os/azurelinux/etc/os-release
@@ -0,0 +1,9 @@
+NAME="Microsoft Azure Linux"
+VERSION="3.0.20240417"
+ID=azurelinux
+VERSION_ID="3.0"
+PRETTY_NAME="Microsoft Azure Linux 3.0"
+ANSI_COLOR="1;34"
+HOME_URL="https://aka.ms/azurelinux"
+BUG_REPORT_URL="https://aka.ms/azurelinux"
+SUPPORT_URL="https://aka.ms/azurelinux"
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/distro/type.go b/grype/distro/type.go
index f079b6ea260..69c73c98322 100644
--- a/grype/distro/type.go
+++ b/grype/distro/type.go
@@ -25,6 +25,7 @@ const (
Photon Type = "photon"
Windows Type = "windows"
Mariner Type = "mariner"
+ Azure Type = "azurelinux"
RockyLinux Type = "rockylinux"
AlmaLinux Type = "almalinux"
Gentoo Type = "gentoo"
@@ -49,6 +50,7 @@ var All = []Type{
Photon,
Windows,
Mariner,
+ Azure,
RockyLinux,
AlmaLinux,
Gentoo,
@@ -64,6 +66,7 @@ var IDMapping = map[string]Type{
"centos": CentOS,
"fedora": Fedora,
"alpine": Alpine,
+ "Alpine Linux": Alpine,
"busybox": Busybox,
"amzn": AmazonLinux,
"ol": OracleLinux,
@@ -73,6 +76,7 @@ var IDMapping = map[string]Type{
"photon": Photon,
"windows": Windows,
"mariner": Mariner,
+ "azurelinux": Azure,
"rocky": RockyLinux,
"almalinux": AlmaLinux,
"gentoo": Gentoo,
diff --git a/grype/event/event.go b/grype/event/event.go
index a95f68b2544..6f0e36f8861 100644
--- a/grype/event/event.go
+++ b/grype/event/event.go
@@ -2,12 +2,10 @@ package event
import (
"github.com/wagoodman/go-partybus"
-
- "github.com/anchore/grype/internal"
)
const (
- typePrefix = internal.ApplicationName
+ typePrefix = "grype"
cliTypePrefix = typePrefix + "-cli"
// Events from the grype library
@@ -26,7 +24,4 @@ const (
// CLINotification is a partybus event that occurs when auxiliary information is ready for presentation to stderr
CLINotification partybus.EventType = cliTypePrefix + "-notification"
-
- // CLIExit is a partybus event that occurs when an analysis result is ready for final presentation
- CLIExit partybus.EventType = cliTypePrefix + "-exit-event"
)
diff --git a/grype/event/monitor/matching.go b/grype/event/monitor/matching.go
index 28967521174..f8280b09e36 100644
--- a/grype/event/monitor/matching.go
+++ b/grype/event/monitor/matching.go
@@ -7,8 +7,10 @@ import (
)
type Matching struct {
- PackagesProcessed progress.Monitorable
- VulnerabilitiesDiscovered progress.Monitorable
- Fixed progress.Monitorable
- BySeverity map[vulnerability.Severity]progress.Monitorable
+ PackagesProcessed progress.Progressable
+ MatchesDiscovered progress.Monitorable
+ Fixed progress.Monitorable
+ Ignored progress.Monitorable
+ Dropped progress.Monitorable
+ BySeverity map[vulnerability.Severity]progress.Monitorable
}
diff --git a/grype/event/parsers/parsers.go b/grype/event/parsers/parsers.go
index a12b4187dae..9606ef1a95f 100644
--- a/grype/event/parsers/parsers.go
+++ b/grype/event/parsers/parsers.go
@@ -74,17 +74,22 @@ func ParseDatabaseDiffingStarted(e partybus.Event) (*monitor.DBDiff, error) {
return &mon, nil
}
-func ParseCLIAppUpdateAvailable(e partybus.Event) (string, error) {
+type UpdateCheck struct {
+ New string
+ Current string
+}
+
+func ParseCLIAppUpdateAvailable(e partybus.Event) (*UpdateCheck, error) {
if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil {
- return "", err
+ return nil, err
}
- newVersion, ok := e.Value.(string)
+ updateCheck, ok := e.Value.(UpdateCheck)
if !ok {
- return "", newPayloadErr(e.Type, "Value", e.Value)
+ return nil, newPayloadErr(e.Type, "Value", e.Value)
}
- return newVersion, nil
+ return &updateCheck, nil
}
func ParseCLIReport(e partybus.Event) (string, string, error) {
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/cpe_target_software_to_pkg_type.go b/grype/internal/cpe_target_software_to_pkg_type.go
new file mode 100644
index 00000000000..8ed402c6a91
--- /dev/null
+++ b/grype/internal/cpe_target_software_to_pkg_type.go
@@ -0,0 +1,58 @@
+package internal
+
+import (
+ "strings"
+
+ "github.com/anchore/syft/syft/pkg"
+)
+
+// CPETargetSoftwareToPackageType is derived from looking at target_software attributes in the NVD dataset
+// TODO: ideally this would be driven from the store, where we can resolve ecosystem aliases directly
+func CPETargetSoftwareToPackageType(tsw string) pkg.Type {
+ tsw = strings.NewReplacer("-", "_", " ", "_").Replace(strings.ToLower(tsw))
+ switch tsw {
+ case "alpine", "apk":
+ return pkg.ApkPkg
+ case "debian", "dpkg":
+ return pkg.DebPkg
+ case "java", "maven", "ant", "gradle", "jenkins", "jenkins_ci", "kafka", "logstash", "mule", "nifi", "solr", "spark", "storm", "struts", "tomcat", "zookeeper", "log4j":
+ return pkg.JavaPkg
+ case "javascript", "node", "nodejs", "node.js", "npm", "yarn", "apache", "jquery", "next.js", "prismjs":
+ return pkg.NpmPkg
+ case "c", "c++", "c/c++", "conan", "gnu_c++", "qt":
+ return pkg.ConanPkg
+ case "dart":
+ return pkg.DartPubPkg
+ case "redhat", "rpm", "redhat_enterprise_linux", "rhel", "suse", "suse_linux", "opensuse", "opensuse_linux", "fedora", "centos", "oracle_linux", "ol":
+ return pkg.RpmPkg
+ case "elixir", "hex":
+ return pkg.HexPkg
+ case "erlang":
+ return pkg.ErlangOTPPkg
+ case ".net", ".net_framework", "asp", "asp.net", "dotnet", "dotnet_framework", "c#", "csharp", "nuget":
+ return pkg.DotnetPkg
+ case "ruby", "gem", "nokogiri", "ruby_on_rails":
+ return pkg.GemPkg
+ case "rust", "cargo", "crates":
+ return pkg.RustPkg
+ case "python", "pip", "pypi", "flask":
+ return pkg.PythonPkg
+ case "kb", "knowledgebase", "msrc", "mskb", "microsoft":
+ return pkg.KbPkg
+ case "portage", "gentoo":
+ return pkg.PortagePkg
+ case "go", "golang", "gomodule":
+ return pkg.GoModulePkg
+ case "linux_kernel", "linux", "z/linux":
+ return pkg.LinuxKernelPkg
+ case "php":
+ return pkg.PhpComposerPkg
+ case "swift":
+ return pkg.SwiftPkg
+ case "wordpress", "wordpress_plugin", "wordpress_":
+ return pkg.WordpressPluginPkg
+ case "lua", "luarocks":
+ return pkg.LuaRocksPkg
+ }
+ return ""
+}
diff --git a/grype/internal/generate.go b/grype/internal/generate.go
new file mode 100644
index 00000000000..72895379e7f
--- /dev/null
+++ b/grype/internal/generate.go
@@ -0,0 +1,3 @@
+package internal
+
+//go:generate go run ./packagemetadata/generate/main.go
diff --git a/grype/internal/packagemetadata/discover_type_names.go b/grype/internal/packagemetadata/discover_type_names.go
new file mode 100644
index 00000000000..a59748758c3
--- /dev/null
+++ b/grype/internal/packagemetadata/discover_type_names.go
@@ -0,0 +1,153 @@
+package packagemetadata
+
+import (
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strings"
+ "unicode"
+
+ "github.com/scylladb/go-set/strset"
+)
+
+var metadataExceptions = strset.New(
+ "FileMetadata",
+ "SBOMFileMetadata",
+ "PURLLiteralMetadata",
+ "CPELiteralMetadata",
+)
+
+func DiscoverTypeNames() ([]string, error) {
+ root, err := RepoRoot()
+ if err != nil {
+ return nil, err
+ }
+ files, err := filepath.Glob(filepath.Join(root, "grype/pkg/*.go"))
+ if err != nil {
+ return nil, err
+ }
+ return findMetadataDefinitionNames(files...)
+}
+
+func RepoRoot() (string, error) {
+ root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
+ if err != nil {
+ return "", fmt.Errorf("unable to find repo root dir: %+v", err)
+ }
+ absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))
+ if err != nil {
+ return "", fmt.Errorf("unable to get abs path to repo root: %w", err)
+ }
+ return absRepoRoot, nil
+}
+
+func findMetadataDefinitionNames(paths ...string) ([]string, error) {
+ names := strset.New()
+ usedNames := strset.New()
+ for _, path := range paths {
+ metadataDefinitions, usedTypeNames, err := findMetadataDefinitionNamesInFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ // useful for debugging...
+ // fmt.Println(path)
+ // fmt.Println("Defs:", metadataDefinitions)
+ // fmt.Println("Used Types:", usedTypeNames)
+ // fmt.Println()
+
+ names.Add(metadataDefinitions...)
+ usedNames.Add(usedTypeNames...)
+ }
+
+ // any definition that is used within another struct should not be considered a top-level metadata definition
+ names.Remove(usedNames.List()...)
+
+ strNames := names.List()
+ sort.Strings(strNames)
+
+ // note: 3 is a point-in-time gut check. This number could be updated if new metadata definitions are added, but is not required.
+ // it is really intended to catch any major issues with the generation process that would generate, say, 0 definitions.
+ if len(strNames) < 3 {
+ return nil, fmt.Errorf("not enough metadata definitions found: discovered %d ", len(strNames))
+ }
+
+ return strNames, nil
+}
+
+func findMetadataDefinitionNamesInFile(path string) ([]string, []string, error) {
+ // set up the parser
+ fs := token.NewFileSet()
+ f, err := parser.ParseFile(fs, path, nil, parser.ParseComments)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var metadataDefinitions []string
+ var usedTypeNames []string
+ for _, decl := range f.Decls {
+ // check if the declaration is a type declaration
+ spec, ok := decl.(*ast.GenDecl)
+ if !ok || spec.Tok != token.TYPE {
+ continue
+ }
+
+ // loop over all types declared in the type declaration
+ for _, typ := range spec.Specs {
+ // check if the type is a struct type
+ spec, ok := typ.(*ast.TypeSpec)
+ if !ok || spec.Type == nil {
+ continue
+ }
+
+ structType, ok := spec.Type.(*ast.StructType)
+ if !ok {
+ continue
+ }
+
+ // check if the struct type ends with "Metadata"
+ name := spec.Name.String()
+
+ // only look for exported types that end with "Metadata"
+ if isMetadataTypeCandidate(name) {
+ // print the full declaration of the struct type
+ metadataDefinitions = append(metadataDefinitions, name)
+ usedTypeNames = append(usedTypeNames, typeNamesUsedInStruct(structType)...)
+ }
+ }
+ }
+ return metadataDefinitions, usedTypeNames, nil
+}
+
+func typeNamesUsedInStruct(structType *ast.StructType) []string {
+ // recursively find all type names used in the struct type
+ var names []string
+ for i := range structType.Fields.List {
+ // capture names of all of the types (not field names)
+ ast.Inspect(structType.Fields.List[i].Type, func(n ast.Node) bool {
+ ident, ok := n.(*ast.Ident)
+ if !ok {
+ return true
+ }
+
+ // add the type name to the list
+ names = append(names, ident.Name)
+
+ // continue inspecting
+ return true
+ })
+ }
+
+ return names
+}
+
+func isMetadataTypeCandidate(name string) bool {
+ return len(name) > 0 &&
+ strings.HasSuffix(name, "Metadata") &&
+ unicode.IsUpper(rune(name[0])) && // must be exported
+ !metadataExceptions.Has(name)
+}
diff --git a/grype/internal/packagemetadata/generate/main.go b/grype/internal/packagemetadata/generate/main.go
new file mode 100644
index 00000000000..0b1d9700c8e
--- /dev/null
+++ b/grype/internal/packagemetadata/generate/main.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/dave/jennifer/jen"
+
+ "github.com/anchore/grype/grype/internal/packagemetadata"
+)
+
+// This program is invoked from grype/internal and generates packagemetadata/generated.go
+
+const (
+ pkgImport = "github.com/anchore/grype/grype/pkg"
+ path = "packagemetadata/generated.go"
+)
+
+func main() {
+ typeNames, err := packagemetadata.DiscoverTypeNames()
+ if err != nil {
+ panic(fmt.Errorf("unable to get all metadata type names: %w", err))
+ }
+
+ fmt.Printf("updating package metadata type list with %+v types\n", len(typeNames))
+
+ f := jen.NewFile("packagemetadata")
+ f.HeaderComment("DO NOT EDIT: generated by grype/internal/packagemetadata/generate/main.go")
+ f.ImportName(pkgImport, "pkg")
+ f.Comment("AllTypes returns a list of all pkg metadata types that grype supports (that are represented in the pkg.Package.Metadata field).")
+
+ f.Func().Id("AllTypes").Params().Index().Any().BlockFunc(func(g *jen.Group) {
+ g.ReturnFunc(func(g *jen.Group) {
+ g.Index().Any().ValuesFunc(func(g *jen.Group) {
+ for _, typeName := range typeNames {
+ g.Qual(pkgImport, typeName).Values()
+ }
+ })
+ })
+ })
+
+ rendered := fmt.Sprintf("%#v", f)
+
+ fh, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ panic(fmt.Errorf("unable to open file: %w", err))
+ }
+ _, err = fh.WriteString(rendered)
+ if err != nil {
+ panic(fmt.Errorf("unable to write file: %w", err))
+ }
+ if err := fh.Close(); err != nil {
+ panic(fmt.Errorf("unable to close file: %w", err))
+ }
+}
diff --git a/grype/internal/packagemetadata/generated.go b/grype/internal/packagemetadata/generated.go
new file mode 100644
index 00000000000..c70c2be3ef0
--- /dev/null
+++ b/grype/internal/packagemetadata/generated.go
@@ -0,0 +1,10 @@
+// DO NOT EDIT: generated by grype/internal/packagemetadata/generate/main.go
+
+package packagemetadata
+
+import "github.com/anchore/grype/grype/pkg"
+
+// AllTypes returns a list of all pkg metadata types that grype supports (that are represented in the pkg.Package.Metadata field).
+func AllTypes() []any {
+ return []any{pkg.ApkMetadata{}, pkg.GolangBinMetadata{}, pkg.GolangModMetadata{}, pkg.JavaMetadata{}, pkg.JavaVMInstallationMetadata{}, pkg.RpmMetadata{}}
+}
diff --git a/grype/internal/packagemetadata/names.go b/grype/internal/packagemetadata/names.go
new file mode 100644
index 00000000000..75e01a0da10
--- /dev/null
+++ b/grype/internal/packagemetadata/names.go
@@ -0,0 +1,83 @@
+package packagemetadata
+
+import (
+ "reflect"
+ "sort"
+ "strings"
+
+ "github.com/anchore/grype/grype/pkg"
+)
+
+// jsonNameFromType is a map of all known package metadata types to their current JSON name and all previously known aliases.
+// TODO: in the future the metadata type names should match how it is used in syft. However, since the data shapes are
+// not the same it may be important to select different names. This design decision has been deferred, for now
+// the same metadata types that have been used in the past should be used here.
+var jsonNameFromType = map[reflect.Type][]string{
+ reflect.TypeOf(pkg.ApkMetadata{}): nameList("ApkMetadata"),
+ reflect.TypeOf(pkg.GolangBinMetadata{}): nameList("GolangBinMetadata"),
+ reflect.TypeOf(pkg.GolangModMetadata{}): nameList("GolangModMetadata"),
+ reflect.TypeOf(pkg.JavaMetadata{}): nameList("JavaMetadata"),
+ reflect.TypeOf(pkg.RpmMetadata{}): nameList("RpmMetadata"),
+ reflect.TypeOf(pkg.JavaVMInstallationMetadata{}): nameList("JavaVMInstallationMetadata"),
+}
+
+//nolint:unparam
+func nameList(id string, others ...string) []string {
+ names := []string{id}
+ for _, o := range others {
+ names = append(names, expandLegacyNameVariants(o)...)
+ }
+ return names
+}
+
+func expandLegacyNameVariants(name string) []string {
+ candidates := []string{name}
+ if strings.HasSuffix(name, "MetadataType") {
+ candidates = append(candidates, strings.TrimSuffix(name, "Type"))
+ } else if strings.HasSuffix(name, "Metadata") {
+ candidates = append(candidates, name+"Type")
+ }
+ return candidates
+}
+
+func AllTypeNames() []string {
+ names := make([]string, 0)
+ for _, t := range AllTypes() {
+ names = append(names, reflect.TypeOf(t).Name())
+ }
+ return names
+}
+
+func JSONName(metadata any) string {
+ if vs, exists := jsonNameFromType[reflect.TypeOf(metadata)]; exists {
+ return vs[0]
+ }
+ return ""
+}
+
+func ReflectTypeFromJSONName(name string) reflect.Type {
+ name = strings.ToLower(name)
+ for _, t := range sortedTypes(jsonNameFromType) {
+ vs := jsonNameFromType[t]
+ for _, v := range vs {
+ if strings.ToLower(v) == name {
+ return t
+ }
+ }
+ }
+ return nil
+}
+
+func sortedTypes(typeNameMapping map[reflect.Type][]string) []reflect.Type {
+ types := make([]reflect.Type, 0)
+ for t := range typeNameMapping {
+ types = append(types, t)
+ }
+
+ // sort the types by their first JSON name
+ sort.Slice(types, func(i, j int) bool {
+ return typeNameMapping[types[i]][0] < typeNameMapping[types[j]][0]
+ })
+
+ return types
+}
diff --git a/grype/internal/packagemetadata/names_test.go b/grype/internal/packagemetadata/names_test.go
new file mode 100644
index 00000000000..f1d4e9333b7
--- /dev/null
+++ b/grype/internal/packagemetadata/names_test.go
@@ -0,0 +1,73 @@
+package packagemetadata
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/pkg"
+)
+
+func TestAllNames(t *testing.T) {
+ // note: this is a form of completion testing relative to the current code base.
+
+ expected, err := DiscoverTypeNames()
+ require.NoError(t, err)
+
+ actual := AllTypeNames()
+
+ // ensure that the codebase (from ast analysis) reflects the latest code generated state
+ if !assert.ElementsMatch(t, expected, actual) {
+ t.Errorf("metadata types not fully represented: \n%s", cmp.Diff(expected, actual))
+ t.Log("did you add a new pkg.*Metadata type without updating the JSON schema?")
+ t.Log("if so, you need to update the schema version and regenerate the JSON schema (make generate-json-schema)")
+ }
+
+ for _, ty := range AllTypes() {
+ assert.NotEmpty(t, JSONName(ty), "metadata type %q does not have a JSON name", ty)
+ }
+}
+
+func TestReflectTypeFromJSONName(t *testing.T) {
+
+ tests := []struct {
+ name string
+ lookup string
+ wantRecord reflect.Type
+ }{
+ {
+ name: "GolangBinMetadata lookup",
+ lookup: "GolangBinMetadata",
+ wantRecord: reflect.TypeOf(pkg.GolangBinMetadata{}),
+ },
+ {
+ name: "GolangModMetadata lookup",
+ lookup: "GolangModMetadata",
+ wantRecord: reflect.TypeOf(pkg.GolangModMetadata{}),
+ },
+ {
+ name: "JavaMetadata lookup",
+ lookup: "JavaMetadata",
+ wantRecord: reflect.TypeOf(pkg.JavaMetadata{}),
+ },
+ {
+ name: "RpmMetadata lookup",
+ lookup: "RpmMetadata",
+ wantRecord: reflect.TypeOf(pkg.RpmMetadata{}),
+ },
+ {
+ name: "JavaVMInstallationMetadata lookup",
+ lookup: "JavaVMInstallationMetadata",
+ wantRecord: reflect.TypeOf(pkg.JavaVMInstallationMetadata{}),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := ReflectTypeFromJSONName(tt.lookup)
+ assert.Equal(t, tt.wantRecord.Name(), got.Name())
+ })
+ }
+}
diff --git a/grype/lib.go b/grype/lib.go
index c1c0a0d3d02..53a499a2515 100644
--- a/grype/lib.go
+++ b/grype/lib.go
@@ -13,5 +13,5 @@ func SetLogger(l logger.Logger) {
}
func SetBus(b *partybus.Bus) {
- bus.SetPublisher(b)
+ bus.Set(b)
}
diff --git a/grype/load_vulnerability_db.go b/grype/load_vulnerability_db.go
index 4b1ea2cf9e1..286fcdb1e4b 100644
--- a/grype/load_vulnerability_db.go
+++ b/grype/load_vulnerability_db.go
@@ -1,44 +1,49 @@
package grype
import (
- "github.com/anchore/grype/grype/db"
- "github.com/anchore/grype/grype/store"
+ "fmt"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+ v6dist "github.com/anchore/grype/grype/db/v6/distribution"
+ v6inst "github.com/anchore/grype/grype/db/v6/installation"
+ "github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/log"
)
-func LoadVulnerabilityDB(cfg db.Config, update bool) (*store.Store, *db.Status, *db.Closer, error) {
- dbCurator, err := db.NewCurator(cfg)
+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)
+ }
+ c, err := v6inst.NewCurator(installCfg, client)
if err != nil {
- return nil, nil, nil, err
+ return nil, nil, fmt.Errorf("unable to create curator: %w", err)
}
if update {
- log.Debug("looking for updates on vulnerability database")
- _, err := dbCurator.Update()
+ updated, err := c.Update()
if err != nil {
- return nil, nil, nil, err
+ if distCfg.RequireUpdateCheck {
+ return nil, nil, fmt.Errorf("unable to update db: %w", err)
+ }
+ log.WithFields("error", err).Warn("error updating db")
}
+ if !updated {
+ log.Debug("no db update found")
+ }
+ } else {
+ log.Debug("skipping db update")
}
- storeReader, dbCloser, err := dbCurator.GetStore()
- if err != nil {
- return nil, nil, nil, err
+ s := c.Status()
+ if s.Error != nil {
+ return nil, nil, s.Error
}
- status := dbCurator.Status()
-
- p, err := db.NewVulnerabilityProvider(storeReader)
+ rdr, err := c.Reader()
if err != nil {
- return nil, &status, nil, err
- }
-
- s := &store.Store{
- Provider: p,
- MetadataProvider: db.NewVulnerabilityMetadataProvider(storeReader),
- ExclusionProvider: db.NewMatchExclusionProvider(storeReader),
+ return nil, nil, fmt.Errorf("unable to create db reader: %w", err)
}
- closer := &db.Closer{DBCloser: dbCloser}
-
- return s, &status, closer, nil
+ return v6.NewVulnerabilityProvider(rdr), &s, nil
}
diff --git a/grype/load_vulnerability_db_bench_test.go b/grype/load_vulnerability_db_bench_test.go
new file mode 100644
index 00000000000..95a7e9fcfb1
--- /dev/null
+++ b/grype/load_vulnerability_db_bench_test.go
@@ -0,0 +1,30 @@
+package grype
+
+import (
+ "math"
+ "path/filepath"
+ "testing"
+
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ "github.com/anchore/grype/grype/db/v6/installation"
+)
+
+// this benchmark was added to measure the performance
+// of LoadVulnerabilityDB, specifically regarding hash validation.
+// https://github.com/anchore/grype/issues/1502
+func BenchmarkLoadVulnerabilityDB(b *testing.B) {
+ for range b.N {
+ _, _, err := LoadVulnerabilityDB(distribution.Config{
+ LatestURL: distribution.DefaultConfig().LatestURL,
+ }, installation.Config{
+ DBRootDir: filepath.Join(".tmp", "grype-db"),
+ ValidateAge: false,
+ ValidateChecksum: true,
+ MaxAllowedBuiltAge: math.MaxInt32,
+ UpdateCheckMaxFrequency: math.MaxInt32,
+ }, true)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/grype/match/details.go b/grype/match/details.go
index 1b6527658d3..5c7ac53fbe3 100644
--- a/grype/match/details.go
+++ b/grype/match/details.go
@@ -2,8 +2,9 @@ package match
import (
"fmt"
+ "strings"
- "github.com/mitchellh/hashstructure/v2"
+ "github.com/gohugoio/hashstructure"
)
type Details []Detail
@@ -42,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,
})
@@ -52,3 +53,38 @@ func (m Detail) ID() string {
return fmt.Sprintf("%x", f)
}
+
+func (m Details) Len() int {
+ return len(m)
+}
+
+func (m Details) Less(i, j int) bool {
+ a := m[i]
+ b := m[j]
+
+ if a.Type != b.Type {
+ // exact-direct-match < exact-indirect-match < cpe-match
+
+ at := typeOrder[a.Type]
+ bt := typeOrder[b.Type]
+ if at == 0 {
+ return false
+ } else if bt == 0 {
+ return true
+ }
+ return at < bt
+ }
+
+ // sort by confidence
+ if a.Confidence != b.Confidence {
+ // flipped comparison since we want higher confidence to be first
+ return a.Confidence > b.Confidence
+ }
+
+ // if the types are the same, then sort by the ID (costly, but deterministic)
+ return strings.Compare(a.ID(), b.ID()) < 0
+}
+
+func (m Details) Swap(i, j int) {
+ m[i], m[j] = m[j], m[i]
+}
diff --git a/grype/match/details_test.go b/grype/match/details_test.go
new file mode 100644
index 00000000000..a7ba5f473e3
--- /dev/null
+++ b/grype/match/details_test.go
@@ -0,0 +1,158 @@
+package match
+
+import (
+ "sort"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDetails_Sorting(t *testing.T) {
+
+ detailExactDirectHigh := Detail{
+ Type: ExactDirectMatch,
+ Confidence: 0.9,
+ SearchedBy: "attribute1",
+ Found: "value1",
+ Matcher: "matcher1",
+ }
+ detailExactDirectLow := Detail{
+ Type: ExactDirectMatch,
+ Confidence: 0.5,
+ SearchedBy: "attribute1",
+ Found: "value1",
+ Matcher: "matcher1",
+ }
+ detailExactIndirect := Detail{
+ Type: ExactIndirectMatch,
+ Confidence: 0.7,
+ SearchedBy: "attribute2",
+ Found: "value2",
+ Matcher: "matcher2",
+ }
+ detailCPEMatch := Detail{
+ Type: CPEMatch,
+ Confidence: 0.8,
+ SearchedBy: "attribute3",
+ Found: "value3",
+ Matcher: "matcher3",
+ }
+
+ tests := []struct {
+ name string
+ details Details
+ expected Details
+ }{
+ {
+ name: "sorts by type first, then by confidence",
+ details: Details{
+ detailCPEMatch,
+ detailExactDirectHigh,
+ detailExactIndirect,
+ detailExactDirectLow,
+ },
+ expected: Details{
+ detailExactDirectHigh,
+ detailExactDirectLow,
+ detailExactIndirect,
+ detailCPEMatch,
+ },
+ },
+ {
+ name: "sorts by confidence within the same type",
+ details: Details{
+ detailExactDirectLow,
+ detailExactDirectHigh,
+ },
+ expected: Details{
+ detailExactDirectHigh,
+ detailExactDirectLow,
+ },
+ },
+ {
+ name: "sorts by ID when type and confidence are the same",
+ details: Details{
+ // clone of detailExactDirectLow with slight difference to enforce ID sorting
+ {
+ Type: ExactDirectMatch,
+ Confidence: 0.5,
+ SearchedBy: "attribute2",
+ Found: "value2",
+ Matcher: "matcher2",
+ },
+ detailExactDirectLow,
+ },
+ expected: Details{
+ detailExactDirectLow,
+ {
+ Type: ExactDirectMatch,
+ Confidence: 0.5,
+ SearchedBy: "attribute2",
+ Found: "value2",
+ Matcher: "matcher2",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ sort.Sort(tt.details)
+ require.Equal(t, tt.expected, tt.details)
+ })
+ }
+}
+
+func TestHasExclusivelyAnyMatchTypes(t *testing.T) {
+ tests := []struct {
+ name string
+ details Details
+ types []Type
+ expected bool
+ }{
+ {
+ name: "all types allowed",
+ details: Details{{Type: "A"}, {Type: "B"}},
+ types: []Type{"A", "B"},
+ expected: true,
+ },
+ {
+ name: "mixed types with disallowed",
+ details: Details{{Type: "A"}, {Type: "B"}, {Type: "C"}},
+ types: []Type{"A", "B"},
+ expected: false,
+ },
+ {
+ name: "single allowed type",
+ details: Details{{Type: "A"}},
+ types: []Type{"A"},
+ expected: true,
+ },
+ {
+ name: "empty details",
+ details: Details{},
+ types: []Type{"A"},
+ expected: false,
+ },
+ {
+ name: "empty types list",
+ details: Details{{Type: "A"}},
+ types: []Type{},
+ expected: false,
+ },
+ {
+ name: "no match with disallowed type",
+ details: Details{{Type: "C"}},
+ types: []Type{"A", "B"},
+ expected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := hasExclusivelyAnyMatchTypes(tt.details, tt.types...)
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
diff --git a/grype/match/explicit_ignores.go b/grype/match/explicit_ignores.go
index 0ec272b1a90..e1d7d02403c 100644
--- a/grype/match/explicit_ignores.go
+++ b/grype/match/explicit_ignores.go
@@ -51,6 +51,12 @@ func init() {
vulnerabilities: []string{"CVE-2017-14727"},
packages: []string{"logger"},
},
+ // https://github.com/anchore/grype/issues/2412#issuecomment-2663656195
+ {
+ typ: "deb",
+ vulnerabilities: []string{"CVE-2023-45853"},
+ packages: []string{"zlib1g", "zlib"},
+ },
}
for _, ignore := range explicitIgnores {
@@ -73,15 +79,17 @@ func ApplyExplicitIgnoreRules(provider ExclusionProvider, matches Matches) (Matc
var ignoreRules []IgnoreRule
ignoreRules = append(ignoreRules, explicitIgnoreRules...)
- for _, m := range matches.Sorted() {
- r, err := provider.GetRules(m.Vulnerability.ID)
+ if provider != nil {
+ for _, m := range matches.Sorted() {
+ r, err := provider.IgnoreRules(m.Vulnerability.ID)
- if err != nil {
- log.Warnf("unable to get ignore rules for vuln id=%s", m.Vulnerability.ID)
- continue
- }
+ if err != nil {
+ log.Warnf("unable to get ignore rules for vuln id=%s", m.Vulnerability.ID)
+ continue
+ }
- ignoreRules = append(ignoreRules, r...)
+ ignoreRules = append(ignoreRules, r...)
+ }
}
return ApplyIgnoreRules(matches, ignoreRules)
diff --git a/grype/match/explicit_ignores_test.go b/grype/match/explicit_ignores_test.go
index c95930de7c4..c699f22ad9a 100644
--- a/grype/match/explicit_ignores_test.go
+++ b/grype/match/explicit_ignores_test.go
@@ -25,7 +25,7 @@ func newMockExclusionProvider() *mockExclusionProvider {
func (d *mockExclusionProvider) stub() {
}
-func (d *mockExclusionProvider) GetRules(vulnerabilityID string) ([]IgnoreRule, error) {
+func (d *mockExclusionProvider) IgnoreRules(vulnerabilityID string) ([]IgnoreRule, error) {
return d.data[vulnerabilityID], nil
}
@@ -117,7 +117,7 @@ func Test_ApplyExplicitIgnoreRules(t *testing.T) {
Type: test.typ,
},
Vulnerability: vulnerability.Vulnerability{
- ID: cp.cve,
+ Reference: vulnerability.Reference{ID: cp.cve},
},
})
}
diff --git a/grype/match/fingerprint.go b/grype/match/fingerprint.go
index 21434e2d21b..cec830fcd09 100644
--- a/grype/match/fingerprint.go
+++ b/grype/match/fingerprint.go
@@ -3,15 +3,19 @@ package match
import (
"fmt"
- "github.com/mitchellh/hashstructure/v2"
+ "github.com/gohugoio/hashstructure"
"github.com/anchore/grype/grype/pkg"
)
type Fingerprint struct {
+ coreFingerprint
+ vulnerabilityFixes string
+}
+
+type coreFingerprint struct {
vulnerabilityID string
vulnerabilityNamespace string
- vulnerabilityFixes string
packageID pkg.ID // note: this encodes package name, version, type, location
}
@@ -20,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/ignore.go b/grype/match/ignore.go
index cd719a70392..83bb38f81fd 100644
--- a/grype/match/ignore.go
+++ b/grype/match/ignore.go
@@ -1,9 +1,20 @@
package match
import (
+ "regexp"
+
"github.com/bmatcuk/doublestar/v2"
+
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
)
+// IgnoreFilter implementations are used to filter matches, returning all applicable IgnoreRule(s) that applied,
+// these could include an IgnoreRule with only a Reason value filled in for synthetically generated rules
+type IgnoreFilter interface {
+ IgnoreMatch(match Match) []IgnoreRule
+}
+
// An IgnoredMatch is a vulnerability Match that has been ignored because one or more IgnoreRules applied to the match.
type IgnoredMatch struct {
Match
@@ -17,19 +28,24 @@ type IgnoredMatch struct {
// specified criteria must be met by the vulnerability match in order for the
// rule to apply.
type IgnoreRule struct {
- Vulnerability string `yaml:"vulnerability" json:"vulnerability" mapstructure:"vulnerability"`
- Namespace string `yaml:"namespace" json:"namespace" mapstructure:"namespace"`
- FixState string `yaml:"fix-state" json:"fix-state" mapstructure:"fix-state"`
- Package IgnoreRulePackage `yaml:"package" json:"package" mapstructure:"package"`
+ Vulnerability string `yaml:"vulnerability" json:"vulnerability" mapstructure:"vulnerability"`
+ Reason string `yaml:"reason" json:"reason" mapstructure:"reason"`
+ Namespace string `yaml:"namespace" json:"namespace" mapstructure:"namespace"`
+ FixState string `yaml:"fix-state" json:"fix-state" mapstructure:"fix-state"`
+ Package IgnoreRulePackage `yaml:"package" json:"package" mapstructure:"package"`
+ VexStatus string `yaml:"vex-status" json:"vex-status" mapstructure:"vex-status"`
+ VexJustification string `yaml:"vex-justification" json:"vex-justification" mapstructure:"vex-justification"`
+ MatchType Type `yaml:"match-type" json:"match-type" mapstructure:"match-type"`
}
// IgnoreRulePackage describes the Package-specific fields that comprise the IgnoreRule.
type IgnoreRulePackage struct {
- Name string `yaml:"name" json:"name" mapstructure:"name"`
- Version string `yaml:"version" json:"version" mapstructure:"version"`
- Language string `yaml:"language" json:"language" mapstructure:"language"`
- Type string `yaml:"type" json:"type" mapstructure:"type"`
- Location string `yaml:"location" json:"location" mapstructure:"location"`
+ Name string `yaml:"name" json:"name" mapstructure:"name"`
+ Version string `yaml:"version" json:"version" mapstructure:"version"`
+ Language string `yaml:"language" json:"language" mapstructure:"language"`
+ Type string `yaml:"type" json:"type" mapstructure:"type"`
+ Location string `yaml:"location" json:"location" mapstructure:"location"`
+ UpstreamName string `yaml:"upstream-name" json:"upstream-name" mapstructure:"upstream-name"`
}
// ApplyIgnoreRules iterates through the provided matches and, for each match,
@@ -39,16 +55,21 @@ type IgnoreRulePackage struct {
// ApplyIgnoreRules returns two collections: the matches that are not being
// ignored, and the matches that are being ignored.
func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMatch) {
+ matched, ignored := ApplyIgnoreFilters(matches.Sorted(), rules...)
+ return NewMatches(matched...), ignored
+}
+
+// ApplyIgnoreFilters applies all the IgnoreFilter(s) to the provided set of matches,
+// splitting the results into a set of matched matches and ignored matches
+func ApplyIgnoreFilters[T IgnoreFilter](matches []Match, filters ...T) ([]Match, []IgnoredMatch) {
+ var out []Match
var ignoredMatches []IgnoredMatch
- remainingMatches := NewMatches()
- for _, match := range matches.Sorted() {
+ for _, match := range matches {
var applicableRules []IgnoreRule
- for _, rule := range rules {
- if shouldIgnore(match, rule) {
- applicableRules = append(applicableRules, rule)
- }
+ for _, filter := range filters {
+ applicableRules = append(applicableRules, filter.IgnoreMatch(match)...)
}
if len(applicableRules) > 0 {
@@ -60,30 +81,56 @@ func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMa
continue
}
- remainingMatches.Add(match)
+ out = append(out, match)
}
- return remainingMatches, ignoredMatches
+ return out, ignoredMatches
}
-func shouldIgnore(match Match, rule IgnoreRule) bool {
- ignoreConditions := getIgnoreConditionsForRule(rule)
+func (r IgnoreRule) IgnoreMatch(match Match) []IgnoreRule {
+ // VEX rules are handled by the vex processor
+ if r.VexStatus != "" {
+ return nil
+ }
+
+ ignoreConditions := getIgnoreConditionsForRule(r)
if len(ignoreConditions) == 0 {
// this rule specifies no criteria, so it doesn't apply to the Match
- return false
+ return nil
}
for _, condition := range ignoreConditions {
if !condition(match) {
// as soon as one rule criterion doesn't apply, we know this rule doesn't apply to the Match
- return false
+ return nil
}
}
// all criteria specified in the rule apply to this Match
- return true
+ return []IgnoreRule{r}
+}
+
+// HasConditions returns true if the ignore rule has conditions
+// that can cause a match to be ignored
+func (r IgnoreRule) HasConditions() bool {
+ return len(getIgnoreConditionsForRule(r)) == 0
}
+// ignoreFilters implements match.IgnoreFilter on a slice of objects that implement the same interface
+type ignoreFilters[T IgnoreFilter] []T
+
+func (r ignoreFilters[T]) IgnoreMatch(match Match) []IgnoreRule {
+ for _, rule := range r {
+ ignores := rule.IgnoreMatch(match)
+ if len(ignores) > 0 {
+ return ignores
+ }
+ }
+ return nil
+}
+
+var _ IgnoreFilter = (*ignoreFilters[IgnoreRule])(nil)
+
// An ignoreCondition is a function that returns a boolean indicating whether
// the given Match should be ignored.
type ignoreCondition func(match Match) bool
@@ -123,11 +170,22 @@ func getIgnoreConditionsForRule(rule IgnoreRule) []ignoreCondition {
ignoreConditions = append(ignoreConditions, ifFixStateApplies(fs))
}
+ if upstreamName := rule.Package.UpstreamName; upstreamName != "" {
+ ignoreConditions = append(ignoreConditions, ifUpstreamPackageNameApplies(upstreamName))
+ }
+
+ if matchType := rule.MatchType; matchType != "" {
+ ignoreConditions = append(ignoreConditions, ifMatchTypeApplies(matchType))
+ }
return ignoreConditions
}
func ifFixStateApplies(fs string) ignoreCondition {
return func(match Match) bool {
+ if fs == string(vulnerability.FixStateUnknown) &&
+ match.Vulnerability.Fix.State == "" { // no fix state specified is effectively "unknown"
+ return true
+ }
return fs == string(match.Vulnerability.Fix.State)
}
}
@@ -144,9 +202,22 @@ func ifNamespaceApplies(namespace string) ignoreCondition {
}
}
+func packageNameRegex(packageName string) (*regexp.Regexp, error) {
+ pattern := packageName
+ if packageName[0] != '$' || packageName[len(packageName)-1] != '^' {
+ pattern = "^" + packageName + "$"
+ }
+ return regexp.Compile(pattern)
+}
+
func ifPackageNameApplies(name string) ignoreCondition {
+ pattern, err := packageNameRegex(name)
+ if err != nil {
+ return func(Match) bool { return false }
+ }
+
return func(match Match) bool {
- return name == match.Package.Name
+ return pattern.MatchString(match.Package.Name)
}
}
@@ -174,13 +245,40 @@ func ifPackageLocationApplies(location string) ignoreCondition {
}
}
+func ifUpstreamPackageNameApplies(name string) ignoreCondition {
+ pattern, err := packageNameRegex(name)
+ if err != nil {
+ log.WithFields("name", name, "error", err).Debug("unable to parse name expression")
+ return func(Match) bool { return false }
+ }
+ return func(match Match) bool {
+ for _, upstream := range match.Package.Upstreams {
+ if pattern.MatchString(upstream.Name) {
+ return true
+ }
+ }
+ return false
+ }
+}
+
+func ifMatchTypeApplies(matchType Type) ignoreCondition {
+ return func(match Match) bool {
+ for _, mType := range match.Details.Types() {
+ if mType == matchType {
+ return true
+ }
+ }
+ return false
+ }
+}
+
func ruleLocationAppliesToMatch(location string, match Match) bool {
for _, packageLocation := range match.Package.Locations.ToSlice() {
if ruleLocationAppliesToPath(location, packageLocation.RealPath) {
return true
}
- if ruleLocationAppliesToPath(location, packageLocation.VirtualPath) {
+ if ruleLocationAppliesToPath(location, packageLocation.AccessPath) {
return true
}
}
diff --git a/grype/match/ignore_test.go b/grype/match/ignore_test.go
index 57af7c53d20..c4e92114b72 100644
--- a/grype/match/ignore_test.go
+++ b/grype/match/ignore_test.go
@@ -6,7 +6,6 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
- grypeDb "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/file"
@@ -17,10 +16,12 @@ var (
allMatches = []Match{
{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-123",
- Namespace: "debian-vulns",
+ Reference: vulnerability.Reference{
+ ID: "CVE-123",
+ Namespace: "debian-vulns",
+ },
Fix: vulnerability.Fix{
- State: grypeDb.FixedState,
+ State: vulnerability.FixStateFixed,
},
},
Package: pkg.Package{
@@ -33,10 +34,12 @@ var (
},
{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-456",
- Namespace: "ruby-vulns",
+ Reference: vulnerability.Reference{
+ ID: "CVE-456",
+ Namespace: "ruby-vulns",
+ },
Fix: vulnerability.Fix{
- State: grypeDb.NotFixedState,
+ State: vulnerability.FixStateNotFixed,
},
},
Package: pkg.Package{
@@ -51,10 +54,12 @@ var (
},
{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-457",
- Namespace: "ruby-vulns",
+ Reference: vulnerability.Reference{
+ ID: "CVE-457",
+ Namespace: "ruby-vulns",
+ },
Fix: vulnerability.Fix{
- State: grypeDb.WontFixState,
+ State: vulnerability.FixStateWontFix,
},
},
Package: pkg.Package{
@@ -69,10 +74,12 @@ var (
},
{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-458",
- Namespace: "ruby-vulns",
+ Reference: vulnerability.Reference{
+ ID: "CVE-458",
+ Namespace: "ruby-vulns",
+ },
Fix: vulnerability.Fix{
- State: grypeDb.UnknownFixState,
+ State: vulnerability.FixStateUnknown,
},
},
Package: pkg.Package{
@@ -86,6 +93,203 @@ var (
},
},
}
+
+ // For testing the match-type rules
+ matchTypesMatches = []Match{
+ // Direct match, not like a normal kernel header match
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-1",
+ Namespace: "fake-redhat-vulns",
+ },
+ Fix: vulnerability.Fix{
+ State: vulnerability.FixStateUnknown,
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "kernel-headers1",
+ Version: "5.1.0",
+ Type: syftPkg.RpmPkg,
+ Upstreams: []pkg.UpstreamPackage{
+ {Name: "kernel2"},
+ },
+ },
+ Details: []Detail{
+ {
+ Type: ExactDirectMatch,
+ },
+ },
+ },
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2",
+ Namespace: "fake-deb-vulns",
+ },
+ Fix: vulnerability.Fix{
+ State: vulnerability.FixStateUnknown,
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "kernel-headers2",
+ Version: "5.1.0",
+ Type: syftPkg.DebPkg,
+ Upstreams: []pkg.UpstreamPackage{
+ {Name: "kernel2"},
+ },
+ },
+ Details: []Detail{
+ {
+ Type: ExactIndirectMatch,
+ },
+ },
+ },
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-1",
+ Namespace: "npm-vulns",
+ },
+ Fix: vulnerability.Fix{
+ State: vulnerability.FixStateUnknown,
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "npm1",
+ Version: "5.1.0",
+ Type: syftPkg.NpmPkg,
+ },
+ Details: []Detail{
+ {
+ Type: CPEMatch,
+ },
+ },
+ },
+ }
+
+ // For testing the match-type and upstream ignore rules
+ kernelHeadersMatches = []Match{
+ // RPM-like match similar to what we see from RedHat
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2",
+ Namespace: "fake-redhat-vulns",
+ },
+ Fix: vulnerability.Fix{
+ State: vulnerability.FixStateUnknown,
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "kernel-headers",
+ Version: "5.1.0",
+ Type: syftPkg.RpmPkg,
+ Upstreams: []pkg.UpstreamPackage{
+ {Name: "kernel"},
+ },
+ },
+ Details: []Detail{
+ {
+ Type: ExactIndirectMatch,
+ },
+ },
+ },
+ // debian-like match, showing the kernel header package name w/embedded version
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2",
+ Namespace: "fake-debian-vulns",
+ },
+ Fix: vulnerability.Fix{
+ State: vulnerability.FixStateUnknown,
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "linux-headers-5.2.0",
+ Version: "5.2.1",
+ Type: syftPkg.DebPkg,
+ Upstreams: []pkg.UpstreamPackage{
+ {Name: "linux"},
+ },
+ },
+ Details: []Detail{
+ {
+ Type: ExactIndirectMatch,
+ },
+ },
+ },
+ // linux-like match, similar to what we see from debian\ubuntu
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-3",
+ Namespace: "fake-linux-vulns",
+ },
+ Fix: vulnerability.Fix{
+ State: vulnerability.FixStateUnknown,
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "linux-azure-headers-generic",
+ Version: "5.2.1",
+ Type: syftPkg.DebPkg,
+ Upstreams: []pkg.UpstreamPackage{
+ {Name: "linux-azure"},
+ },
+ },
+ Details: []Detail{
+ {
+ Type: ExactIndirectMatch,
+ },
+ },
+ },
+ }
+
+ // For testing the match-type and upstream ignore rules
+ packageTypeMatches = []Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2",
+ Namespace: "fake-redhat-vulns",
+ },
+ Fix: vulnerability.Fix{
+ State: vulnerability.FixStateUnknown,
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "kernel-headers",
+ Version: "5.1.0",
+ Type: syftPkg.RpmPkg,
+ },
+ },
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2",
+ Namespace: "fake-debian-vulns",
+ },
+ Fix: vulnerability.Fix{
+ State: vulnerability.FixStateUnknown,
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "linux-headers-5.2.0",
+ Version: "5.2.1",
+ Type: syftPkg.DebPkg,
+ },
+ },
+ }
)
func TestApplyIgnoreRules(t *testing.T) {
@@ -193,9 +397,9 @@ func TestApplyIgnoreRules(t *testing.T) {
name: "ignore matches without fix",
allMatches: allMatches,
ignoreRules: []IgnoreRule{
- {FixState: string(grypeDb.NotFixedState)},
- {FixState: string(grypeDb.WontFixState)},
- {FixState: string(grypeDb.UnknownFixState)},
+ {FixState: string(vulnerability.FixStateNotFixed)},
+ {FixState: string(vulnerability.FixStateWontFix)},
+ {FixState: string(vulnerability.FixStateUnknown)},
},
expectedRemainingMatches: []Match{
allMatches[0],
@@ -309,6 +513,250 @@ func TestApplyIgnoreRules(t *testing.T) {
},
},
},
+ {
+ name: "ignore matches on indirect match-type",
+ allMatches: matchTypesMatches,
+ ignoreRules: []IgnoreRule{
+ {
+ MatchType: ExactIndirectMatch,
+ },
+ },
+ expectedRemainingMatches: []Match{
+ matchTypesMatches[0], matchTypesMatches[2],
+ },
+ expectedIgnoredMatches: []IgnoredMatch{
+ {
+ Match: matchTypesMatches[1],
+ AppliedIgnoreRules: []IgnoreRule{
+ {
+ MatchType: ExactIndirectMatch,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "ignore matches on cpe match-type",
+ allMatches: matchTypesMatches,
+ ignoreRules: []IgnoreRule{
+ {
+ MatchType: CPEMatch,
+ },
+ },
+ expectedRemainingMatches: []Match{
+ matchTypesMatches[0], matchTypesMatches[1],
+ },
+ expectedIgnoredMatches: []IgnoredMatch{
+ {
+ Match: matchTypesMatches[2],
+ AppliedIgnoreRules: []IgnoreRule{
+ {
+ MatchType: CPEMatch,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "ignore matches on upstream name",
+ allMatches: kernelHeadersMatches,
+ ignoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ UpstreamName: "kernel",
+ },
+ },
+ {
+ Package: IgnoreRulePackage{
+ UpstreamName: "linux-.*",
+ },
+ },
+ },
+ expectedRemainingMatches: []Match{
+ kernelHeadersMatches[1],
+ },
+ expectedIgnoredMatches: []IgnoredMatch{
+ {
+ Match: kernelHeadersMatches[0],
+ AppliedIgnoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ UpstreamName: "kernel",
+ },
+ },
+ },
+ },
+ {
+ Match: kernelHeadersMatches[2],
+ AppliedIgnoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ UpstreamName: "linux-.*",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "ignore matches on package type",
+ allMatches: packageTypeMatches,
+ ignoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Type: string(syftPkg.RpmPkg),
+ },
+ },
+ },
+ expectedRemainingMatches: []Match{
+ packageTypeMatches[1],
+ },
+ expectedIgnoredMatches: []IgnoredMatch{
+ {
+ Match: packageTypeMatches[0],
+ AppliedIgnoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Type: string(syftPkg.RpmPkg),
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "ignore matches rpms for kernel-headers with kernel upstream",
+ allMatches: kernelHeadersMatches,
+ ignoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Name: "kernel-headers",
+ UpstreamName: "kernel",
+ Type: string(syftPkg.RpmPkg),
+ },
+ MatchType: ExactIndirectMatch,
+ },
+ {
+ Package: IgnoreRulePackage{
+ Name: "linux-.*-headers-.*",
+ UpstreamName: "linux.*",
+ Type: string(syftPkg.DebPkg),
+ },
+ MatchType: ExactIndirectMatch,
+ },
+ },
+ expectedRemainingMatches: []Match{
+ kernelHeadersMatches[1],
+ },
+ expectedIgnoredMatches: []IgnoredMatch{
+ {
+ Match: kernelHeadersMatches[0],
+ AppliedIgnoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Name: "kernel-headers",
+ UpstreamName: "kernel",
+ Type: string(syftPkg.RpmPkg),
+ },
+ MatchType: ExactIndirectMatch,
+ },
+ },
+ },
+ {
+ Match: kernelHeadersMatches[2],
+ AppliedIgnoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Name: "linux-.*-headers-.*",
+ UpstreamName: "linux.*",
+ Type: string(syftPkg.DebPkg),
+ },
+ MatchType: ExactIndirectMatch,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "ignore on name regex",
+ allMatches: kernelHeadersMatches,
+ ignoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Name: "kernel-headers.*",
+ },
+ },
+ },
+ expectedRemainingMatches: []Match{
+ kernelHeadersMatches[1],
+ kernelHeadersMatches[2],
+ },
+ expectedIgnoredMatches: []IgnoredMatch{
+ {
+ Match: kernelHeadersMatches[0],
+ AppliedIgnoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Name: "kernel-headers.*",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "ignore on name regex, no matches",
+ allMatches: kernelHeadersMatches,
+ ignoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Name: "foo.*",
+ },
+ },
+ },
+ expectedRemainingMatches: kernelHeadersMatches,
+ expectedIgnoredMatches: nil,
+ },
+ {
+ name: "ignore on name regex, line termination verification",
+ allMatches: kernelHeadersMatches,
+ ignoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Name: "^kernel-header$",
+ },
+ },
+ },
+ expectedRemainingMatches: kernelHeadersMatches,
+ expectedIgnoredMatches: nil,
+ },
+ {
+ name: "ignore on name regex, line termination test match",
+ allMatches: kernelHeadersMatches,
+ ignoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Name: "^kernel-headers$",
+ },
+ },
+ },
+ expectedRemainingMatches: []Match{
+ kernelHeadersMatches[1],
+ kernelHeadersMatches[2],
+ },
+ expectedIgnoredMatches: []IgnoredMatch{
+ {
+ Match: kernelHeadersMatches[0],
+ AppliedIgnoreRules: []IgnoreRule{
+ {
+ Package: IgnoreRulePackage{
+ Name: "^kernel-headers$",
+ },
+ },
+ },
+ },
+ },
+ },
}
for _, testCase := range cases {
@@ -331,7 +779,7 @@ func sliceToMatches(s []Match) Matches {
var (
exampleMatch = Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2000-1234",
+ Reference: vulnerability.Reference{ID: "CVE-2000-1234"},
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -412,7 +860,7 @@ func TestShouldIgnore(t *testing.T) {
match: exampleMatch,
rule: IgnoreRule{
Package: IgnoreRulePackage{
- Location: exampleMatch.Package.Locations.ToSlice()[1].VirtualPath,
+ Location: exampleMatch.Package.Locations.ToSlice()[1].AccessPath,
},
},
expected: true,
@@ -454,7 +902,7 @@ func TestShouldIgnore(t *testing.T) {
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
- actual := shouldIgnore(testCase.match, testCase.rule)
+ actual := len(testCase.rule.IgnoreMatch(testCase.match)) > 0
assert.Equal(t, testCase.expected, actual)
})
}
diff --git a/grype/match/match.go b/grype/match/match.go
index 128ceae169f..f9c5c469bed 100644
--- a/grype/match/match.go
+++ b/grype/match/match.go
@@ -18,7 +18,7 @@ var ErrCannotMerge = fmt.Errorf("unable to merge vulnerability matches")
type Match struct {
Vulnerability vulnerability.Vulnerability // The vulnerability details of the match.
Package pkg.Package // The package used to search for a match.
- Details Details // all ways in which how this particular match was made.
+ Details Details // all the ways this particular match was made.
}
// String is the string representation of select match fields.
@@ -26,16 +26,14 @@ func (m Match) String() string {
return fmt.Sprintf("Match(pkg=%s vuln=%q types=%q)", m.Package, m.Vulnerability.String(), m.Details.Types())
}
-func (m Match) Summary() string {
- return fmt.Sprintf("vuln=%q matchers=%s", m.Vulnerability.ID, m.Details.Matchers())
-}
-
func (m Match) Fingerprint() Fingerprint {
return Fingerprint{
- vulnerabilityID: m.Vulnerability.ID,
- vulnerabilityNamespace: m.Vulnerability.Namespace,
- vulnerabilityFixes: strings.Join(m.Vulnerability.Fix.Versions, ","),
- packageID: m.Package.ID,
+ coreFingerprint: coreFingerprint{
+ vulnerabilityID: m.Vulnerability.ID,
+ vulnerabilityNamespace: m.Vulnerability.Namespace,
+ packageID: m.Package.ID,
+ },
+ vulnerabilityFixes: strings.Join(m.Vulnerability.Fix.Versions, ","),
}
}
@@ -77,11 +75,7 @@ func (m *Match) Merge(other Match) error {
}
// for stable output
- sort.Slice(m.Details, func(i, j int) bool {
- a := m.Details[i]
- b := m.Details[j]
- return strings.Compare(a.ID(), b.ID()) < 0
- })
+ sort.Sort(m.Details)
// retain all unique CPEs for consistent output
m.Vulnerability.CPEs = cpe.Merge(m.Vulnerability.CPEs, other.Vulnerability.CPEs)
diff --git a/grype/match/match_test.go b/grype/match/match_test.go
new file mode 100644
index 00000000000..76e6e1940d7
--- /dev/null
+++ b/grype/match/match_test.go
@@ -0,0 +1,249 @@
+package match
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/syft/syft/cpe"
+)
+
+func TestMatch_Merge(t *testing.T) {
+ tests := []struct {
+ name string
+ m1 Match
+ m2 Match
+ expectedErr error
+ expected Match
+ }{
+ {
+ name: "error on fingerprint mismatch",
+ m1: Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-0001",
+ Namespace: "namespace1",
+ },
+ },
+ Package: pkg.Package{
+ ID: "pkg1",
+ },
+ },
+ m2: Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-0002",
+ Namespace: "namespace2",
+ },
+ },
+ Package: pkg.Package{
+ ID: "pkg2",
+ },
+ },
+ expectedErr: ErrCannotMerge,
+ },
+ {
+ name: "merge with unique values",
+ m1: Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-0001",
+ Namespace: "namespace",
+ },
+ RelatedVulnerabilities: []vulnerability.Reference{
+ {
+ Namespace: "ns1",
+ ID: "ID1",
+ },
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource),
+ },
+ },
+ Package: pkg.Package{
+ ID: "pkg1",
+ },
+ Details: Details{
+ {
+ Type: ExactDirectMatch,
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ },
+ },
+ },
+ m2: Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-0001",
+ Namespace: "namespace",
+ },
+ RelatedVulnerabilities: []vulnerability.Reference{
+ {
+ Namespace: "ns2",
+ ID: "ID2",
+ },
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:example:example:1.1:*:*:*:*:*:*:*", cpe.DeclaredSource),
+ },
+ },
+ Package: pkg.Package{
+ ID: "pkg1",
+ },
+ Details: Details{
+ {
+ Type: ExactIndirectMatch,
+ SearchedBy: "attr2",
+ Found: "value2",
+ Matcher: "matcher2",
+ },
+ },
+ },
+ expectedErr: nil,
+ expected: Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-0001",
+ Namespace: "namespace",
+ },
+ RelatedVulnerabilities: []vulnerability.Reference{
+ {
+ Namespace: "ns1",
+ ID: "ID1",
+ },
+ {
+ Namespace: "ns2",
+ ID: "ID2",
+ },
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource),
+ cpe.Must("cpe:2.3:a:example:example:1.1:*:*:*:*:*:*:*", cpe.DeclaredSource),
+ },
+ },
+ Package: pkg.Package{
+ ID: "pkg1",
+ },
+ Details: Details{
+ {
+ Type: ExactDirectMatch,
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ },
+ {
+ Type: ExactIndirectMatch,
+ SearchedBy: "attr2",
+ Found: "value2",
+ Matcher: "matcher2",
+ },
+ },
+ },
+ },
+ {
+ name: "merges with duplicate values",
+ m1: Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-0001",
+ Namespace: "namespace",
+ },
+ RelatedVulnerabilities: []vulnerability.Reference{
+ {
+ Namespace: "ns1",
+ ID: "ID1",
+ },
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource),
+ },
+ },
+ Package: pkg.Package{
+ ID: "pkg1",
+ },
+ Details: Details{
+ {
+ Type: ExactDirectMatch,
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ },
+ },
+ },
+ m2: Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-0001",
+ Namespace: "namespace",
+ },
+ RelatedVulnerabilities: []vulnerability.Reference{
+ {
+ Namespace: "ns1",
+ ID: "ID1",
+ },
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource),
+ },
+ },
+ Package: pkg.Package{
+ ID: "pkg1",
+ },
+ Details: Details{
+ {
+ Type: ExactDirectMatch,
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ },
+ },
+ },
+ expectedErr: nil,
+ expected: Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-0001",
+ Namespace: "namespace",
+ },
+ RelatedVulnerabilities: []vulnerability.Reference{
+ {
+ Namespace: "ns1",
+ ID: "ID1",
+ },
+ },
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource),
+ },
+ },
+ Package: pkg.Package{
+ ID: "pkg1",
+ },
+ Details: Details{
+ {
+ Type: ExactDirectMatch,
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := tt.m1.Merge(tt.m2)
+ if tt.expectedErr != nil {
+ require.ErrorIs(t, err, tt.expectedErr)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tt.expected.Vulnerability.RelatedVulnerabilities, tt.m1.Vulnerability.RelatedVulnerabilities)
+ require.Equal(t, tt.expected.Details, tt.m1.Details)
+ require.Equal(t, tt.expected.Vulnerability.CPEs, tt.m1.Vulnerability.CPEs)
+ }
+ })
+ }
+}
diff --git a/grype/match/matcher.go b/grype/match/matcher.go
new file mode 100644
index 00000000000..ceb0e42183d
--- /dev/null
+++ b/grype/match/matcher.go
@@ -0,0 +1,44 @@
+package match
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/vulnerability"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+// Matcher is the interface to implement to provide top-level package-to-match
+type Matcher interface {
+ PackageTypes() []syftPkg.Type
+
+ Type() MatcherType
+
+ // Match is called for every package found, returning any matches and an optional Ignorer which will be applied
+ // 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/matcher_type.go b/grype/match/matcher_type.go
index 6b596a88521..cc0aa412102 100644
--- a/grype/match/matcher_type.go
+++ b/grype/match/matcher_type.go
@@ -14,6 +14,8 @@ const (
MsrcMatcher MatcherType = "msrc-matcher"
PortageMatcher MatcherType = "portage-matcher"
GoModuleMatcher MatcherType = "go-module-matcher"
+ OpenVexMatcher MatcherType = "openvex-matcher"
+ RustMatcher MatcherType = "rust-matcher"
)
var AllMatcherTypes = []MatcherType{
@@ -28,6 +30,12 @@ var AllMatcherTypes = []MatcherType{
MsrcMatcher,
PortageMatcher,
GoModuleMatcher,
+ OpenVexMatcher,
+ RustMatcher,
}
type MatcherType string
+
+func (t MatcherType) String() string {
+ return string(t)
+}
diff --git a/grype/match/matches.go b/grype/match/matches.go
index 0df9a977884..7ce27f6b852 100644
--- a/grype/match/matches.go
+++ b/grype/match/matches.go
@@ -3,13 +3,16 @@ package match
import (
"sort"
+ "github.com/scylladb/go-set/strset"
+
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/internal/log"
)
type Matches struct {
- byFingerprint map[Fingerprint]Match
- byPackage map[pkg.ID][]Fingerprint
+ byFingerprint map[Fingerprint]Match
+ byCoreFingerprint map[coreFingerprint]map[Fingerprint]struct{}
+ byPackage map[pkg.ID]map[Fingerprint]struct{}
}
func NewMatches(matches ...Match) Matches {
@@ -20,14 +23,15 @@ func NewMatches(matches ...Match) Matches {
func newMatches() Matches {
return Matches{
- byFingerprint: make(map[Fingerprint]Match),
- byPackage: make(map[pkg.ID][]Fingerprint),
+ byFingerprint: make(map[Fingerprint]Match),
+ byCoreFingerprint: make(map[coreFingerprint]map[Fingerprint]struct{}),
+ byPackage: make(map[pkg.ID]map[Fingerprint]struct{}),
}
}
// GetByPkgID returns a slice of potential matches from an ID
func (r *Matches) GetByPkgID(id pkg.ID) (matches []Match) {
- for _, fingerprint := range r.byPackage[id] {
+ for fingerprint := range r.byPackage[id] {
matches = append(matches, r.byFingerprint[fingerprint])
}
return matches
@@ -37,7 +41,7 @@ func (r *Matches) GetByPkgID(id pkg.ID) (matches []Match) {
func (r *Matches) AllByPkgID() map[pkg.ID][]Match {
matches := make(map[pkg.ID][]Match)
for id, fingerprints := range r.byPackage {
- for _, fingerprint := range fingerprints {
+ for fingerprint := range fingerprints {
matches[id] = append(matches[id], r.byFingerprint[fingerprint])
}
}
@@ -46,33 +50,121 @@ func (r *Matches) AllByPkgID() map[pkg.ID][]Match {
func (r *Matches) Merge(other Matches) {
for _, fingerprints := range other.byPackage {
- for _, fingerprint := range fingerprints {
+ for fingerprint := range fingerprints {
r.Add(other.byFingerprint[fingerprint])
}
}
}
-func (r *Matches) Add(matches ...Match) {
- if len(matches) == 0 {
- return
+func (r *Matches) Diff(other Matches) *Matches {
+ diff := newMatches()
+ for fingerprint := range r.byFingerprint {
+ if _, exists := other.byFingerprint[fingerprint]; !exists {
+ diff.Add(r.byFingerprint[fingerprint])
+ }
}
+ return &diff
+}
+
+func (r *Matches) Add(matches ...Match) {
for _, newMatch := range matches {
- fingerprint := newMatch.Fingerprint()
+ newFp := newMatch.Fingerprint()
// add or merge the new match with an existing match
- if existingMatch, exists := r.byFingerprint[fingerprint]; exists {
- if err := existingMatch.Merge(newMatch); err != nil {
- log.Warnf("unable to merge matches: original=%q new=%q : %w", existingMatch.String(), newMatch.String(), err)
- // TODO: dropped match in this case, we should figure a way to handle this
+ r.addOrMerge(newMatch, newFp)
+
+ // track common elements (core fingerprint + package index)
+
+ if _, exists := r.byCoreFingerprint[newFp.coreFingerprint]; !exists {
+ r.byCoreFingerprint[newFp.coreFingerprint] = make(map[Fingerprint]struct{})
+ }
+
+ r.byCoreFingerprint[newFp.coreFingerprint][newFp] = struct{}{}
+
+ if _, exists := r.byPackage[newMatch.Package.ID]; !exists {
+ r.byPackage[newMatch.Package.ID] = make(map[Fingerprint]struct{})
+ }
+ r.byPackage[newMatch.Package.ID][newFp] = struct{}{}
+ }
+}
+
+func (r *Matches) addOrMerge(newMatch Match, newFp Fingerprint) {
+ // a) if there is an exact fingerprint match, then merge with that
+ // b) otherwise, look for core fingerprint matches (looser rules)
+ // we prefer direct matches to indirect matches:
+ // 1. if the new match is a direct match and there is an indirect match, replace the indirect match with the direct match
+ // 2. if the new match is an indirect match and there is a direct match, merge with the existing direct match
+ // c) this is a new match
+
+ if existingMatch, exists := r.byFingerprint[newFp]; exists {
+ // case A
+ if err := existingMatch.Merge(newMatch); err != nil {
+ log.WithFields("original", existingMatch.String(), "new", newMatch.String(), "error", err).Warn("unable to merge matches")
+ // at least capture the additional details
+ existingMatch.Details = append(existingMatch.Details, newMatch.Details...)
+ }
+
+ r.byFingerprint[newFp] = existingMatch
+ } else if existingFingerprints, exists := r.byCoreFingerprint[newFp.coreFingerprint]; exists {
+ // case B
+ if !r.mergeCoreMatches(newMatch, newFp, existingFingerprints) {
+ // case C (we should not drop this match if we were unable to merge it)
+ r.byFingerprint[newFp] = newMatch
+ }
+ } else {
+ // case C
+ r.byFingerprint[newFp] = newMatch
+ }
+}
+
+func (r *Matches) mergeCoreMatches(newMatch Match, newFp Fingerprint, existingFingerprints map[Fingerprint]struct{}) bool {
+ for existingFp := range existingFingerprints {
+ existingMatch := r.byFingerprint[existingFp]
+
+ shouldSupersede := hasMatchType(newMatch.Details, ExactDirectMatch) && hasExclusivelyAnyMatchTypes(existingMatch.Details, ExactIndirectMatch)
+ if shouldSupersede {
+ // 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
}
- r.byFingerprint[fingerprint] = existingMatch
+ }
+
+ // case B2
+ if err := existingMatch.Merge(newMatch); err != nil {
+ 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 {
- r.byFingerprint[fingerprint] = newMatch
+ return true
}
+ }
+ return false
+}
- // keep track of which matches correspond to which packages
- r.byPackage[newMatch.Package.ID] = append(r.byPackage[newMatch.Package.ID], fingerprint)
+func (r *Matches) replace(m Match, ogFp, newFp Fingerprint, extraDetails ...Detail) bool {
+ if ogFp.coreFingerprint != newFp.coreFingerprint {
+ return false
}
+
+ // update indexes
+ for pkgID, fingerprints := range r.byPackage {
+ if _, exists := fingerprints[ogFp]; exists {
+ delete(fingerprints, ogFp)
+ fingerprints[newFp] = struct{}{}
+ r.byPackage[pkgID] = fingerprints
+ }
+ }
+
+ // update the match
+ delete(r.byFingerprint, ogFp)
+ m.Details = append(m.Details, extraDetails...)
+ sort.Sort(m.Details)
+ r.byFingerprint[newFp] = m
+ return true
}
func (r *Matches) Enumerate() <-chan Match {
@@ -101,3 +193,28 @@ func (r *Matches) Sorted() []Match {
func (r *Matches) Count() int {
return len(r.byFingerprint)
}
+
+func hasMatchType(details Details, ty Type) bool {
+ for _, d := range details {
+ if d.Type == ty {
+ return true
+ }
+ }
+ return false
+}
+
+func hasExclusivelyAnyMatchTypes(details Details, tys ...Type) bool {
+ allowed := strset.New()
+ for _, ty := range tys {
+ allowed.Add(string(ty))
+ }
+ var found bool
+ for _, d := range details {
+ if allowed.Has(string(d.Type)) {
+ found = true
+ } else {
+ return false
+ }
+ }
+ return found
+}
diff --git a/grype/match/matches_test.go b/grype/match/matches_test.go
index 59d829335c4..ca1d48481bb 100644
--- a/grype/match/matches_test.go
+++ b/grype/match/matches_test.go
@@ -3,19 +3,25 @@ package match
import (
"testing"
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/syft/syft/file"
syftPkg "github.com/anchore/syft/syft/pkg"
)
func TestMatchesSortMixedDimensions(t *testing.T) {
first := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -26,7 +32,9 @@ func TestMatchesSortMixedDimensions(t *testing.T) {
}
second := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0020",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0020",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -37,7 +45,9 @@ func TestMatchesSortMixedDimensions(t *testing.T) {
}
third := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0020",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0020",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -48,7 +58,9 @@ func TestMatchesSortMixedDimensions(t *testing.T) {
}
fourth := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0020",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0020",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -59,7 +71,9 @@ func TestMatchesSortMixedDimensions(t *testing.T) {
}
fifth := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0020",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0020",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -68,21 +82,89 @@ func TestMatchesSortMixedDimensions(t *testing.T) {
Type: syftPkg.RpmPkg,
},
}
+ sixth := Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0020",
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"2.0.0", "1.0.0"},
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "package-d",
+ Version: "2.0.0",
+ Type: syftPkg.RpmPkg,
+ },
+ }
+ seventh := Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0020",
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"2.0.1"},
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "package-d",
+ Version: "2.0.0",
+ Type: syftPkg.RpmPkg,
+ },
+ }
+ eighth := Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0020",
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"3.0.0"},
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "package-d",
+ Version: "2.0.0",
+ Type: syftPkg.RpmPkg,
+ Locations: file.NewLocationSet(file.NewLocation("/some/first-path")),
+ },
+ }
+ ninth := Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0020",
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"3.0.0"},
+ },
+ },
+ Package: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "package-d",
+ Version: "2.0.0",
+ Type: syftPkg.RpmPkg,
+ Locations: file.NewLocationSet(file.NewLocation("/some/other-path")),
+ },
+ }
input := []Match{
// shuffle vulnerability id, package name, package version, and package type
- fifth, third, first, second, fourth,
+ ninth, fifth, eighth, third, seventh, first, sixth, second, fourth,
}
matches := NewMatches(input...)
- assertMatchOrder(t, []Match{first, second, third, fourth, fifth}, matches.Sorted())
+ assertMatchOrder(t, []Match{first, second, third, fourth, fifth, sixth, seventh, eighth, ninth}, matches.Sorted())
}
func TestMatchesSortByVulnerability(t *testing.T) {
first := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -93,7 +175,9 @@ func TestMatchesSortByVulnerability(t *testing.T) {
}
second := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0020",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0020",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -115,7 +199,9 @@ func TestMatchesSortByVulnerability(t *testing.T) {
func TestMatches_AllByPkgID(t *testing.T) {
first := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -126,7 +212,9 @@ func TestMatches_AllByPkgID(t *testing.T) {
}
second := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -157,7 +245,9 @@ func TestMatches_AllByPkgID(t *testing.T) {
func TestMatchesSortByPackage(t *testing.T) {
first := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -168,7 +258,9 @@ func TestMatchesSortByPackage(t *testing.T) {
}
second := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -190,7 +282,9 @@ func TestMatchesSortByPackage(t *testing.T) {
func TestMatchesSortByPackageVersion(t *testing.T) {
first := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -201,7 +295,9 @@ func TestMatchesSortByPackageVersion(t *testing.T) {
}
second := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -223,7 +319,9 @@ func TestMatchesSortByPackageVersion(t *testing.T) {
func TestMatchesSortByPackageType(t *testing.T) {
first := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -234,7 +332,9 @@ func TestMatchesSortByPackageType(t *testing.T) {
}
second := Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2020-0010",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-0010",
+ },
},
Package: pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -290,3 +390,315 @@ func assertIgnoredMatchOrder(t *testing.T, expected, actual []IgnoredMatch) {
// make certain the fields are what you'd expect
assert.Equal(t, expected, actual)
}
+
+func TestMatches_Diff(t *testing.T) {
+ a := Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "vuln-a",
+ Namespace: "name-a",
+ },
+ },
+ Package: pkg.Package{
+ ID: "package-a",
+ },
+ }
+
+ b := Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "vuln-b",
+ Namespace: "name-b",
+ },
+ },
+ Package: pkg.Package{
+ ID: "package-b",
+ },
+ }
+
+ c := Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "vuln-c",
+ Namespace: "name-c",
+ },
+ },
+ Package: pkg.Package{
+ ID: "package-c",
+ },
+ }
+
+ tests := []struct {
+ name string
+ subject Matches
+ other Matches
+ want Matches
+ }{
+ {
+ name: "no diff",
+ subject: NewMatches(a, b, c),
+ other: NewMatches(a, b, c),
+ want: newMatches(),
+ },
+ {
+ name: "extra items in subject",
+ subject: NewMatches(a, b, c),
+ other: NewMatches(a, b),
+ want: NewMatches(c),
+ },
+ {
+ // this demonstrates that this is not meant to implement a symmetric diff
+ name: "extra items in other (results in no diff)",
+ subject: NewMatches(a, b),
+ other: NewMatches(a, b, c),
+ want: NewMatches(),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equalf(t, &tt.want, tt.subject.Diff(tt.other), "Diff(%v)", tt.other)
+ })
+ }
+}
+
+func TestMatches_Add_Merge(t *testing.T) {
+ commonVuln := "CVE-2023-0001"
+ commonNamespace := "namespace1"
+ commonVulnerability := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: commonVuln,
+ Namespace: commonNamespace,
+ },
+ Constraint: func() version.Constraint {
+ c, err := version.GetConstraint("< 1.0.0", version.SemanticFormat)
+ require.NoError(t, err)
+ return c
+ }(),
+ Fix: vulnerability.Fix{
+ Versions: []string{"1.0.0"},
+ },
+ }
+
+ commonDirectDetail := Detail{
+ Type: ExactDirectMatch,
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ }
+
+ matchPkg1Direct := Match{
+ Vulnerability: commonVulnerability,
+ Package: pkg.Package{
+ ID: "pkg1",
+ },
+ Details: Details{
+ commonDirectDetail,
+ },
+ }
+
+ matchPkg2Indirect := Match{
+ Vulnerability: commonVulnerability,
+ Package: pkg.Package{
+ ID: "pkg2",
+ },
+ Details: Details{
+ {
+ Type: ExactIndirectMatch,
+ SearchedBy: "attr2",
+ Found: "value2",
+ Matcher: "matcher2",
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ matches []Match
+ expectedMatches map[string][]Match
+ }{
+ {
+ name: "adds new match without merging",
+ matches: []Match{matchPkg1Direct, matchPkg2Indirect},
+ expectedMatches: map[string][]Match{
+ "pkg1": {
+ matchPkg1Direct,
+ },
+ "pkg2": {
+ matchPkg2Indirect,
+ },
+ },
+ },
+ {
+ name: "merges matches with identical fingerprints",
+ matches: []Match{
+ matchPkg1Direct,
+ {
+ Vulnerability: matchPkg1Direct.Vulnerability,
+ Package: matchPkg1Direct.Package,
+ Details: Details{
+ {
+ Type: ExactIndirectMatch, // different!
+ SearchedBy: "attr2", // different!
+ Found: "value2", // different!
+ Matcher: "matcher2", // different!
+ },
+ },
+ },
+ },
+ expectedMatches: map[string][]Match{
+ "pkg1": {
+ {
+ Vulnerability: commonVulnerability,
+ Package: matchPkg1Direct.Package,
+ Details: Details{
+ commonDirectDetail,
+ {
+ Type: ExactIndirectMatch,
+ SearchedBy: "attr2",
+ Found: "value2",
+ Matcher: "matcher2",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "merges matches with different fingerprints but semantically the same",
+ matches: []Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: commonVuln,
+ Namespace: commonNamespace,
+ },
+ Constraint: func() version.Constraint { // different!
+ c, err := version.GetConstraint("< 3.2.12", version.SemanticFormat)
+ require.NoError(t, err)
+ return c
+ }(),
+ Fix: vulnerability.Fix{
+ Versions: []string{"3.2.12"}, // different!
+ },
+ },
+ Package: matchPkg1Direct.Package,
+ Details: Details{
+ {
+ Type: ExactIndirectMatch, // different!
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ },
+ },
+ },
+ matchPkg1Direct,
+ },
+ expectedMatches: map[string][]Match{
+ "pkg1": {
+ {
+ Vulnerability: commonVulnerability,
+ Package: matchPkg1Direct.Package,
+ Details: Details{
+ commonDirectDetail, // sorts to first (direct should be prioritized over indirect)
+ {
+ Type: ExactIndirectMatch, // different!
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "does not merge matches with different fingerprints but semantically the same when matched by CPE",
+ matches: []Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: commonVuln,
+ Namespace: commonNamespace,
+ },
+ Constraint: func() version.Constraint { // different!
+ c, err := version.GetConstraint("< 3.2.12", version.SemanticFormat)
+ require.NoError(t, err)
+ return c
+ }(),
+ Fix: vulnerability.Fix{
+ Versions: []string{"3.2.12"}, // different!
+ },
+ },
+ Package: matchPkg1Direct.Package,
+ Details: Details{
+ {
+ Type: CPEMatch, // different!
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ },
+ },
+ },
+ matchPkg1Direct,
+ },
+ expectedMatches: map[string][]Match{
+ "pkg1": {
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: commonVuln,
+ Namespace: commonNamespace,
+ },
+ Constraint: func() version.Constraint { // different!
+ c, err := version.GetConstraint("< 3.2.12", version.SemanticFormat)
+ require.NoError(t, err)
+ return c
+ }(),
+ Fix: vulnerability.Fix{
+ Versions: []string{"3.2.12"}, // different!
+ },
+ },
+ Package: matchPkg1Direct.Package,
+ Details: Details{
+ {
+ Type: CPEMatch, // different!
+ SearchedBy: "attr1",
+ Found: "value1",
+ Matcher: "matcher1",
+ },
+ },
+ },
+ matchPkg1Direct,
+ },
+ },
+ },
+ }
+
+ cmpOpts := []cmp.Option{
+ cmpopts.IgnoreUnexported(vulnerability.Vulnerability{}, pkg.Package{}, file.Location{}, file.LocationSet{}),
+ cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
+ cmpopts.EquateEmpty(),
+ cmpopts.SortSlices(func(a, b Match) bool {
+ return ByElements([]Match{a, b}).Less(0, 1)
+ }),
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ actual := NewMatches(tt.matches...)
+
+ require.NotEmpty(t, tt.expectedMatches)
+
+ for pkgId, expected := range tt.expectedMatches {
+ storedMatches := actual.GetByPkgID(pkg.ID(pkgId))
+
+ if d := cmp.Diff(expected, storedMatches, cmpOpts...); d != "" {
+ t.Errorf("unexpected matches for %q (-want, +got): %s", pkgId, d)
+ }
+ }
+
+ assert.Len(t, actual.byPackage, len(tt.expectedMatches))
+
+ })
+ }
+}
diff --git a/grype/match/provider.go b/grype/match/provider.go
index 10b124aac6e..8b6c3b69914 100644
--- a/grype/match/provider.go
+++ b/grype/match/provider.go
@@ -1,5 +1,5 @@
package match
type ExclusionProvider interface {
- GetRules(vulnerabilityID string) ([]IgnoreRule, error)
+ IgnoreRules(vulnerabilityID string) ([]IgnoreRule, error)
}
diff --git a/grype/match/results.go b/grype/match/results.go
new file mode 100644
index 00000000000..f98dbee5757
--- /dev/null
+++ b/grype/match/results.go
@@ -0,0 +1,56 @@
+package match
+
+import (
+ "fmt"
+ "sort"
+
+ "github.com/scylladb/go-set/strset"
+)
+
+type CPEPackageParameter struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+}
+
+type CPEParameters struct {
+ Namespace string `json:"namespace"`
+ CPEs []string `json:"cpes"`
+ Package CPEPackageParameter `json:"package"`
+}
+
+func (i *CPEParameters) Merge(other CPEParameters) error {
+ if i.Namespace != other.Namespace {
+ return fmt.Errorf("namespaces do not match")
+ }
+
+ existingCPEs := strset.New(i.CPEs...)
+ newCPEs := strset.New(other.CPEs...)
+ mergedCPEs := strset.Union(existingCPEs, newCPEs).List()
+ sort.Strings(mergedCPEs)
+ i.CPEs = mergedCPEs
+ return nil
+}
+
+type CPEResult struct {
+ VulnerabilityID string `json:"vulnerabilityID"`
+ VersionConstraint string `json:"versionConstraint"`
+ CPEs []string `json:"cpes"`
+}
+
+func (h CPEResult) Equals(other CPEResult) bool {
+ if h.VersionConstraint != other.VersionConstraint {
+ return false
+ }
+
+ if len(h.CPEs) != len(other.CPEs) {
+ return false
+ }
+
+ for i := range h.CPEs {
+ if h.CPEs[i] != other.CPEs[i] {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/grype/match/sort.go b/grype/match/sort.go
index c9b79791c22..a4b7e3e5ab6 100644
--- a/grype/match/sort.go
+++ b/grype/match/sort.go
@@ -1,6 +1,9 @@
package match
-import "sort"
+import (
+ "sort"
+ "strings"
+)
var _ sort.Interface = (*ByElements)(nil)
@@ -16,6 +19,32 @@ func (m ByElements) Less(i, j int) bool {
if m[i].Vulnerability.ID == m[j].Vulnerability.ID {
if m[i].Package.Name == m[j].Package.Name {
if m[i].Package.Version == m[j].Package.Version {
+ if m[i].Package.Type == m[j].Package.Type {
+ // this is an approximate ordering, but is not accurate in terms of semver and other version formats
+ // but stability is what is important here, not the accuracy of the sort.
+ fixVersions1 := m[i].Vulnerability.Fix.Versions
+ fixVersions2 := m[j].Vulnerability.Fix.Versions
+ sort.Strings(fixVersions1)
+ sort.Strings(fixVersions2)
+ fixStr1 := strings.Join(fixVersions1, ",")
+ fixStr2 := strings.Join(fixVersions2, ",")
+
+ if fixStr1 == fixStr2 {
+ loc1 := m[i].Package.Locations.ToSlice()
+ loc2 := m[j].Package.Locations.ToSlice()
+ var locStr1 string
+ for _, location := range loc1 {
+ locStr1 += location.RealPath
+ }
+ var locStr2 string
+ for _, location := range loc2 {
+ locStr2 += location.RealPath
+ }
+
+ return locStr1 < locStr2
+ }
+ return fixStr1 < fixStr2
+ }
return m[i].Package.Type < m[j].Package.Type
}
return m[i].Package.Version < m[j].Package.Version
diff --git a/grype/match/type.go b/grype/match/type.go
index 7f4573667a3..e5030d02c2f 100644
--- a/grype/match/type.go
+++ b/grype/match/type.go
@@ -10,8 +10,18 @@ const (
CPEMatch Type = "cpe-match"
)
+var typeOrder = map[Type]int{
+ ExactDirectMatch: 1,
+ ExactIndirectMatch: 2,
+ CPEMatch: 3,
+}
+
type Type string
+func (t Type) String() string {
+ return string(t)
+}
+
func ConvertToIndirectMatches(matches []Match, p pkg.Package) {
for idx := range matches {
for dIdx := range matches[idx].Details {
diff --git a/grype/matcher/apk/matcher.go b/grype/matcher/apk/matcher.go
index 2dc1145248f..7aafeaa65c5 100644
--- a/grype/matcher/apk/matcher.go
+++ b/grype/matcher/apk/matcher.go
@@ -1,14 +1,17 @@
package apk
import (
+ "errors"
"fmt"
"github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -23,46 +26,70 @@ func (m *Matcher) Type() match.MatcherType {
return match.ApkMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
- var matches = make([]match.Match, 0)
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
+ var matches []match.Match
- // direct matches with package
- directMatches, err := m.findApkPackage(store, d, p)
+ // direct matches with package itself
+ directMatches, err := m.findMatchesForPackage(store, p)
if err != nil {
- return nil, err
+ return nil, nil, err
}
matches = append(matches, directMatches...)
- // indirect matches with package source
- indirectMatches, err := m.matchBySourceIndirection(store, d, p)
+ // indirect matches, via package's origin package
+ indirectMatches, err := m.findMatchesForOriginPackage(store, p)
if err != nil {
- return nil, err
+ return nil, nil, err
}
matches = append(matches, indirectMatches...)
- return matches, nil
+ // APK sources are also able to NAK vulnerabilities, so we want to return these as explicit ignores in order
+ // to allow rules later to use these to ignore "the same" vulnerability found in "the same" locations
+ naks, err := m.findNaksForPackage(store, p)
+
+ return matches, naks, err
}
-func (m *Matcher) cpeMatchesWithoutSecDBFixes(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+//nolint:funlen,gocognit
+func (m *Matcher) cpeMatchesWithoutSecDBFixes(provider vulnerability.Provider, p pkg.Package) ([]match.Match, error) {
// find CPE-indexed vulnerability matches specific to the given package name and version
- cpeMatches, err := search.ByPackageCPE(store, d, p, m.Type())
+ cpeMatches, err := internal.MatchPackageByCPEs(provider, p, m.Type())
if err != nil {
- return nil, err
+ log.WithFields("package", p.Name, "error", err).Debug("failed to find CPE matches for package")
+ }
+ if p.Distro == nil {
+ return cpeMatches, nil
}
cpeMatchesByID := matchesByID(cpeMatches)
// remove cpe matches where there is an entry in the secDB for the particular package-vulnerability pairing, and the
// installed package version is >= the fixed in version for the secDB record.
- secDBVulnerabilities, err := store.GetByDistro(d, p)
+ secDBVulnerabilities, err := provider.FindVulnerabilities(
+ search.ByPackageName(p.Name),
+ search.ByDistro(*p.Distro))
if err != nil {
return nil, err
}
+ for _, upstreamPkg := range pkg.UpstreamPackages(p) {
+ secDBVulnerabilitiesForUpstream, err := provider.FindVulnerabilities(
+ search.ByPackageName(upstreamPkg.Name),
+ search.ByDistro(*upstreamPkg.Distro))
+ if err != nil {
+ return nil, err
+ }
+ secDBVulnerabilities = append(secDBVulnerabilities, secDBVulnerabilitiesForUpstream...)
+ }
+
secDBVulnerabilitiesByID := vulnerabilitiesByID(secDBVulnerabilities)
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
+ }
return nil, fmt.Errorf("matcher failed to parse version pkg='%s' ver='%s': %w", p.Name, p.Version, err)
}
@@ -74,7 +101,16 @@ cveLoop:
secDBVulnerabilitiesForID, exists := secDBVulnerabilitiesByID[id]
if !exists {
// does not exist in secdb, so the CPE record(s) should be added to the final results
- finalCpeMatches = append(finalCpeMatches, cpeMatchesForID...)
+
+ // remove fixed-in versions, since NVD doesn't know when Alpine will fix things
+ for _, nvdOnlyMatch := range cpeMatchesForID {
+ if len(nvdOnlyMatch.Vulnerability.Fix.Versions) > 0 {
+ nvdOnlyMatch.Vulnerability.Fix = vulnerability.Fix{
+ State: vulnerability.FixStateUnknown,
+ }
+ }
+ finalCpeMatches = append(finalCpeMatches, nvdOnlyMatch)
+ }
continue
}
@@ -133,15 +169,16 @@ func vulnerabilitiesByID(vulns []vulnerability.Vulnerability) map[string][]vulne
return results
}
-func (m *Matcher) findApkPackage(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
- // find Alpine SecDB matches for the given package name and version
- secDBMatches, err := search.ByPackageDistro(store, d, p, m.Type())
+func (m *Matcher) findMatchesForPackage(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) {
+ // find SecDB matches for the given package name and version
+ secDBMatches, _, err := internal.MatchPackageByDistro(store, p, m.Type())
if err != nil {
return nil, err
}
- cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, d, p)
- if err != nil {
+ // TODO: are there other errors that we should handle here that causes this to short circuit
+ cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, p)
+ if err != nil && !errors.Is(err, internal.ErrEmptyCPEMatch) {
return nil, err
}
@@ -156,11 +193,11 @@ func (m *Matcher) findApkPackage(store vulnerability.Provider, d *distro.Distro,
return matches, nil
}
-func (m *Matcher) matchBySourceIndirection(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) findMatchesForOriginPackage(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) {
var matches []match.Match
for _, indirectPackage := range pkg.UpstreamPackages(p) {
- indirectMatches, err := m.findApkPackage(store, d, indirectPackage)
+ indirectMatches, err := m.findMatchesForPackage(store, indirectPackage)
if err != nil {
return nil, fmt.Errorf("failed to find vulnerabilities for apk upstream source package: %w", err)
}
@@ -173,3 +210,68 @@ func (m *Matcher) matchBySourceIndirection(store vulnerability.Provider, d *dist
return matches, nil
}
+
+// NAK entries are those reported as explicitly not vulnerable by the upstream provider,
+// for example this entry is present in the v5 database:
+// 312891,CVE-2020-7224,openvpn,alpine:distro:alpine:3.10,,< 0,apk,,"[{""id"":""CVE-2020-7224"",""namespace"":""nvd:cpe""}]","[""0""]",fixed,
+// which indicates, for the alpine:3.10 distro, package openvpn is not vulnerable to CVE-2020-7224
+// we want to report these NAK entries as match.IgnoredMatch, to allow for later processing to create ignore rules
+// based on packages which overlap by location, such as a python binary found in addition to the python APK entry --
+// we want to NAK this vulnerability for BOTH packages
+func (m *Matcher) findNaksForPackage(provider vulnerability.Provider, p pkg.Package) ([]match.IgnoredMatch, error) {
+ // TODO: this was only applying to specific distros as originally implemented; this should probably be removed:
+ if d := p.Distro; d == nil || d.Type != distro.Wolfi && d.Type != distro.Chainguard && d.Type != distro.Alpine {
+ return nil, nil
+ }
+
+ // get all the direct naks
+ naks, err := provider.FindVulnerabilities(
+ search.ByDistro(*p.Distro),
+ search.ByPackageName(p.Name),
+ nakConstraint,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // append all the upstream naks
+ for _, upstreamPkg := range pkg.UpstreamPackages(p) {
+ upstreamNaks, err := provider.FindVulnerabilities(
+ search.ByDistro(*upstreamPkg.Distro),
+ search.ByPackageName(upstreamPkg.Name),
+ nakConstraint,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ naks = append(naks, upstreamNaks...)
+ }
+
+ var ignores []match.IgnoredMatch
+ for _, nak := range naks {
+ ignores = append(ignores, match.IgnoredMatch{
+ Match: match.Match{
+ Vulnerability: nak,
+ Package: p,
+ Details: nil, // Probably don't need details here
+ },
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Vulnerability: nak.ID,
+ Reason: "NAK",
+ },
+ },
+ })
+ }
+
+ return ignores, nil
+}
+
+var (
+ nakVersionString = version.MustGetConstraint("< 0", version.ApkFormat).String()
+ // nakConstraint checks the exact version string for being an APK version with "< 0"
+ nakConstraint = search.ByConstraintFunc(func(c version.Constraint) (bool, error) {
+ return c.String() == nakVersionString, nil
+ })
+)
diff --git a/grype/matcher/apk/matcher_test.go b/grype/matcher/apk/matcher_test.go
index 569b94a07fb..4f068cafd02 100644
--- a/grype/matcher/apk/matcher_test.go
+++ b/grype/matcher/apk/matcher_test.go
@@ -9,66 +9,107 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/anchore/grype/grype/db"
- grypeDB "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
"github.com/anchore/syft/syft/cpe"
syftPkg "github.com/anchore/syft/syft/pkg"
)
-type mockStore struct {
- backend map[string]map[string][]grypeDB.Vulnerability
-}
+func TestSecDBOnlyMatch(t *testing.T) {
+ secDbVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ // ID doesn't match - this is the key for comparison in the matcher
+ ID: "CVE-2020-2",
+ Namespace: "secdb:distro:alpine:3.12",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("<= 0.9.11", version.ApkFormat),
+ }
-func (s *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
- //TODO implement me
- panic("implement me")
-}
+ vp := mock.VulnerabilityProvider(secDbVuln)
-func (s *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
- namespaceMap := s.backend[namespace]
- if namespaceMap == nil {
- return nil, nil
+ m := Matcher{}
+ d, err := distro.New(distro.Alpine, "3.12.0", "")
+ if err != nil {
+ t.Fatalf("failed to create a new distro: %+v", err)
}
- return namespaceMap[name], nil
-}
-func (s *mockStore) GetAllVulnerabilities() (*[]grypeDB.Vulnerability, error) {
- return nil, nil
-}
+ p := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "libvncserver",
+ Version: "0.9.9",
+ Type: syftPkg.ApkPkg,
+ Distro: d,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""),
+ },
+ }
+
+ expected := []match.Match{
+ {
-func (s *mockStore) GetVulnerabilityNamespaces() ([]string, error) {
- keys := make([]string, 0, len(s.backend))
- for k := range s.backend {
- keys = append(keys, k)
+ Vulnerability: secDbVuln,
+ Package: p,
+ Details: []match.Detail{
+ {
+ Type: match.ExactDirectMatch,
+ Confidence: 1.0,
+ SearchedBy: map[string]interface{}{
+ "distro": map[string]string{
+ "type": d.Type.String(),
+ "version": d.Version,
+ },
+ "package": map[string]string{
+ "name": "libvncserver",
+ "version": "0.9.9",
+ },
+ "namespace": "secdb:distro:alpine:3.12",
+ },
+ Found: map[string]interface{}{
+ "versionConstraint": secDbVuln.Constraint.String(),
+ "vulnerabilityID": "CVE-2020-2",
+ },
+ Matcher: match.ApkMatcher,
+ },
+ },
+ },
}
- return keys, nil
+ actual, _, err := m.Match(vp, p)
+ assert.NoError(t, err)
+
+ assertMatches(t, expected, actual)
}
-func TestSecDBOnlyMatch(t *testing.T) {
+func TestBothSecdbAndNvdMatches(t *testing.T) {
+ // NVD and Alpine's secDB both have the same CVE ID for the package
+ nvdVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""),
+ },
+ }
- secDbVuln := grypeDB.Vulnerability{
- // ID doesn't match - this is the key for comparison in the matcher
- ID: "CVE-2020-2",
- VersionConstraint: "<= 0.9.11",
- VersionFormat: "apk",
- Namespace: "secdb:distro:alpine:3.12",
- }
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "secdb:distro:alpine:3.12": {
- "libvncserver": []grypeDB.Vulnerability{secDbVuln},
- },
+ secDbVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ // ID *does* match - this is the key for comparison in the matcher
+ ID: "CVE-2020-1",
+ Namespace: "secdb:distro:alpine:3.12",
},
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("<= 0.9.11", version.ApkFormat),
}
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
+ vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)
m := Matcher{}
d, err := distro.New(distro.Alpine, "3.12.0", "")
@@ -81,18 +122,16 @@ func TestSecDBOnlyMatch(t *testing.T) {
Name: "libvncserver",
Version: "0.9.9",
Type: syftPkg.ApkPkg,
+ Distro: d,
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""),
},
}
- vulnFound, err := vulnerability.NewVulnerability(secDbVuln)
- assert.NoError(t, err)
-
expected := []match.Match{
{
-
- Vulnerability: *vulnFound,
+ // ensure the SECDB record is preferred over the NVD record
+ Vulnerability: secDbVuln,
Package: p,
Details: []match.Detail{
{
@@ -101,7 +140,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",
@@ -110,8 +149,8 @@ func TestSecDBOnlyMatch(t *testing.T) {
"namespace": "secdb:distro:alpine:3.12",
},
Found: map[string]interface{}{
- "versionConstraint": vulnFound.Constraint.String(),
- "vulnerabilityID": "CVE-2020-2",
+ "versionConstraint": secDbVuln.Constraint.String(),
+ "vulnerabilityID": "CVE-2020-1",
},
Matcher: match.ApkMatcher,
},
@@ -119,43 +158,45 @@ func TestSecDBOnlyMatch(t *testing.T) {
},
}
- actual, err := m.Match(provider, d, p)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
assertMatches(t, expected, actual)
}
-func TestBothSecdbAndNvdMatches(t *testing.T) {
+func TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) {
// NVD and Alpine's secDB both have the same CVE ID for the package
- nvdVuln := grypeDB.Vulnerability{
- ID: "CVE-2020-1",
- VersionConstraint: "<= 0.9.11",
- VersionFormat: "unknown",
- CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`},
- Namespace: "nvd:cpe",
- }
-
- secDbVuln := grypeDB.Vulnerability{
- // ID *does* match - this is the key for comparison in the matcher
- ID: "CVE-2020-1",
- VersionConstraint: "<= 0.9.11",
- VersionFormat: "apk",
- Namespace: "secdb:distro:alpine:3.12",
- }
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "nvd:cpe": {
- "libvncserver": []grypeDB.Vulnerability{nvdVuln},
- },
- "secdb:distro:alpine:3.12": {
- "libvncserver": []grypeDB.Vulnerability{secDbVuln},
- },
+ nvdVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("< 1.0.0", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""),
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"1.0.0"},
+ State: vulnerability.FixStateFixed,
},
}
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
-
+ secDbVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ // ID *does* match - this is the key for comparison in the matcher
+ ID: "CVE-2020-1",
+ Namespace: "secdb:distro:alpine:3.12",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("< 0.9.12", version.ApkFormat),
+ // SecDB indicates Alpine have backported a fix to v0.9...
+ Fix: vulnerability.Fix{
+ Versions: []string{"0.9.12"},
+ State: vulnerability.FixStateFixed,
+ },
+ }
+ vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)
m := Matcher{}
d, err := distro.New(distro.Alpine, "3.12.0", "")
if err != nil {
@@ -167,19 +208,16 @@ func TestBothSecdbAndNvdMatches(t *testing.T) {
Name: "libvncserver",
Version: "0.9.9",
Type: syftPkg.ApkPkg,
+ Distro: d,
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""),
},
}
- // ensure the SECDB record is preferred over the NVD record
- vulnFound, err := vulnerability.NewVulnerability(secDbVuln)
- assert.NoError(t, err)
-
expected := []match.Match{
{
-
- Vulnerability: *vulnFound,
+ // ensure the SECDB record is preferred over the NVD record
+ Vulnerability: secDbVuln,
Package: p,
Details: []match.Detail{
{
@@ -188,7 +226,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",
@@ -197,7 +235,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) {
"namespace": "secdb:distro:alpine:3.12",
},
Found: map[string]interface{}{
- "versionConstraint": vulnFound.Constraint.String(),
+ "versionConstraint": secDbVuln.Constraint.String(),
"vulnerabilityID": "CVE-2020-1",
},
Matcher: match.ApkMatcher,
@@ -206,7 +244,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) {
},
}
- actual, err := m.Match(provider, d, p)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
assertMatches(t, expected, actual)
@@ -214,35 +252,30 @@ func TestBothSecdbAndNvdMatches(t *testing.T) {
func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
// NVD and Alpine's secDB both have the same CVE ID for the package
- nvdVuln := grypeDB.Vulnerability{
- ID: "CVE-2020-1",
- VersionConstraint: "<= 0.9.11",
- VersionFormat: "unknown",
+ nvdVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat),
// Note: the product name is NOT the same as the target package name
- CPEs: []string{"cpe:2.3:a:lib_vnc_project-(server):libvncumbrellaproject:*:*:*:*:*:*:*:*"},
- Namespace: "nvd:cpe",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:lib_vnc_project-(server):libvncumbrellaproject:*:*:*:*:*:*:*:*", ""),
+ },
}
- secDbVuln := grypeDB.Vulnerability{
- // ID *does* match - this is the key for comparison in the matcher
- ID: "CVE-2020-1",
- VersionConstraint: "<= 0.9.11",
- VersionFormat: "apk",
- Namespace: "secdb:distro:alpine:3.12",
- }
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "nvd:cpe": {
- "libvncumbrellaproject": []grypeDB.Vulnerability{nvdVuln},
- },
- "secdb:distro:alpine:3.12": {
- "libvncserver": []grypeDB.Vulnerability{secDbVuln},
- },
+ secDbVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ // ID *does* match - this is the key for comparison in the matcher
+ ID: "CVE-2020-1",
+ Namespace: "secdb:distro:alpine:3.12",
},
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("<= 0.9.11", version.ApkFormat),
}
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
+ vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)
m := Matcher{}
d, err := distro.New(distro.Alpine, "3.12.0", "")
@@ -254,20 +287,17 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
Name: "libvncserver",
Version: "0.9.9",
Type: syftPkg.ApkPkg,
+ Distro: d,
CPEs: []cpe.CPE{
// Note: the product name is NOT the same as the package name
- cpe.Must("cpe:2.3:a:*:libvncumbrellaproject:0.9.9:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:*:libvncumbrellaproject:0.9.9:*:*:*:*:*:*:*", ""),
},
}
- // ensure the SECDB record is preferred over the NVD record
- vulnFound, err := vulnerability.NewVulnerability(secDbVuln)
- assert.NoError(t, err)
-
expected := []match.Match{
{
-
- Vulnerability: *vulnFound,
+ // ensure the SECDB record is preferred over the NVD record
+ Vulnerability: secDbVuln,
Package: p,
Details: []match.Detail{
{
@@ -276,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",
@@ -285,7 +315,7 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
"namespace": "secdb:distro:alpine:3.12",
},
Found: map[string]interface{}{
- "versionConstraint": vulnFound.Constraint.String(),
+ "versionConstraint": secDbVuln.Constraint.String(),
"vulnerabilityID": "CVE-2020-1",
},
Matcher: match.ApkMatcher,
@@ -294,30 +324,93 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
},
}
- actual, err := m.Match(provider, d, p)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
assertMatches(t, expected, actual)
}
func TestNvdOnlyMatches(t *testing.T) {
- nvdVuln := grypeDB.Vulnerability{
- ID: "CVE-2020-1",
- VersionConstraint: "<= 0.9.11",
- VersionFormat: "unknown",
- CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`},
- Namespace: "nvd:cpe",
- }
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "nvd:cpe": {
- "libvncserver": []grypeDB.Vulnerability{nvdVuln},
+ nvdVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""),
+ },
+ }
+ vp := mock.VulnerabilityProvider(nvdVuln)
+
+ m := Matcher{}
+ d, err := distro.New(distro.Alpine, "3.12.0", "")
+ if err != nil {
+ t.Fatalf("failed to create a new distro: %+v", err)
+ }
+ p := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "libvncserver",
+ Version: "0.9.9",
+ Type: syftPkg.ApkPkg,
+ Distro: d,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""),
+ },
+ }
+
+ expected := []match.Match{
+ {
+
+ Vulnerability: nvdVuln,
+ Package: p,
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "libvncserver",
+ Version: "0.9.9",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{nvdVuln.CPEs[0].Attributes.BindToFmtString()},
+ VersionConstraint: nvdVuln.Constraint.String(),
+ VulnerabilityID: "CVE-2020-1",
+ },
+ Matcher: match.ApkMatcher,
+ },
},
},
}
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
+ actual, _, err := m.Match(vp, p)
+ assert.NoError(t, err)
+
+ assertMatches(t, expected, actual)
+}
+
+func TestNvdOnlyMatches_FixInNvd(t *testing.T) {
+ nvdVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("< 0.9.11", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""),
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"0.9.12"},
+ State: vulnerability.FixStateFixed,
+ },
+ }
+ vp := mock.VulnerabilityProvider(nvdVuln)
m := Matcher{}
d, err := distro.New(distro.Alpine, "3.12.0", "")
@@ -329,34 +422,35 @@ func TestNvdOnlyMatches(t *testing.T) {
Name: "libvncserver",
Version: "0.9.9",
Type: syftPkg.ApkPkg,
+ Distro: d,
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""),
},
}
- vulnFound, err := vulnerability.NewVulnerability(nvdVuln)
- assert.NoError(t, err)
- vulnFound.CPEs = []cpe.CPE{cpe.Must(nvdVuln.CPEs[0])}
+ vulnFound := nvdVuln
+ // Important: for alpine matcher, fix version can come from secDB but _not_ from
+ // NVD data.
+ vulnFound.Fix = vulnerability.Fix{State: vulnerability.FixStateUnknown}
expected := []match.Match{
{
-
- Vulnerability: *vulnFound,
+ Vulnerability: vulnFound,
Package: p,
Details: []match.Detail{
{
Type: match.CPEMatch,
Confidence: 0.9,
- SearchedBy: search.CPEParameters{
+ SearchedBy: match.CPEParameters{
CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"},
Namespace: "nvd:cpe",
- Package: search.CPEPackageParameter{
+ Package: match.CPEPackageParameter{
Name: "libvncserver",
Version: "0.9.9",
},
},
- Found: search.CPEResult{
- CPEs: []string{vulnFound.CPEs[0].BindToFmtString()},
+ Found: match.CPEResult{
+ CPEs: []string{vulnFound.CPEs[0].Attributes.BindToFmtString()},
VersionConstraint: vulnFound.Constraint.String(),
VulnerabilityID: "CVE-2020-1",
},
@@ -366,37 +460,36 @@ func TestNvdOnlyMatches(t *testing.T) {
},
}
- actual, err := m.Match(provider, d, p)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
assertMatches(t, expected, actual)
}
func TestNvdMatchesProperVersionFiltering(t *testing.T) {
- nvdVulnMatch := grypeDB.Vulnerability{
- ID: "CVE-2020-1",
- VersionConstraint: "<= 0.9.11",
- VersionFormat: "unknown",
- CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`},
- Namespace: "nvd:cpe",
- }
- nvdVulnNoMatch := grypeDB.Vulnerability{
- ID: "CVE-2020-2",
- VersionConstraint: "< 0.9.11",
- VersionFormat: "unknown",
- CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`},
- Namespace: "nvd:cpe",
- }
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "nvd:cpe": {
- "libvncserver": []grypeDB.Vulnerability{nvdVulnMatch, nvdVulnNoMatch},
- },
+ nvdVulnMatch := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""),
},
}
-
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
+ nvdVulnNoMatch := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-2",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("< 0.9.11", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""),
+ },
+ }
+ vp := mock.VulnerabilityProvider(nvdVulnMatch, nvdVulnNoMatch)
m := Matcher{}
d, err := distro.New(distro.Alpine, "3.12.0", "")
@@ -408,35 +501,31 @@ func TestNvdMatchesProperVersionFiltering(t *testing.T) {
Name: "libvncserver",
Version: "0.9.11-r10",
Type: syftPkg.ApkPkg,
+ Distro: d,
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*", ""),
},
}
- vulnFound, err := vulnerability.NewVulnerability(nvdVulnMatch)
- assert.NoError(t, err)
- vulnFound.CPEs = []cpe.CPE{cpe.Must(nvdVulnMatch.CPEs[0])}
-
expected := []match.Match{
{
-
- Vulnerability: *vulnFound,
+ Vulnerability: nvdVulnMatch,
Package: p,
Details: []match.Detail{
{
Type: match.CPEMatch,
Confidence: 0.9,
- SearchedBy: search.CPEParameters{
+ SearchedBy: match.CPEParameters{
CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*"},
Namespace: "nvd:cpe",
- Package: search.CPEPackageParameter{
+ Package: match.CPEPackageParameter{
Name: "libvncserver",
Version: "0.9.11-r10",
},
},
- Found: search.CPEResult{
- CPEs: []string{vulnFound.CPEs[0].BindToFmtString()},
- VersionConstraint: vulnFound.Constraint.String(),
+ Found: match.CPEResult{
+ CPEs: []string{nvdVulnMatch.CPEs[0].Attributes.BindToFmtString()},
+ VersionConstraint: nvdVulnMatch.Constraint.String(),
VulnerabilityID: "CVE-2020-1",
},
Matcher: match.ApkMatcher,
@@ -445,40 +534,35 @@ func TestNvdMatchesProperVersionFiltering(t *testing.T) {
},
}
- actual, err := m.Match(provider, d, p)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
assertMatches(t, expected, actual)
}
func TestNvdMatchesWithSecDBFix(t *testing.T) {
- nvdVuln := grypeDB.Vulnerability{
- ID: "CVE-2020-1",
- VersionConstraint: "> 0.9.0, < 0.10.0", // note: this is not normal NVD configuration, but has the desired effect of a "wide net" for vulnerable indication
- VersionFormat: "unknown",
- CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`},
- Namespace: "nvd:cpe",
- }
-
- secDbVuln := grypeDB.Vulnerability{
- ID: "CVE-2020-1",
- VersionConstraint: "< 0.9.11", // note: this does NOT include 0.9.11, so NVD and SecDB mismatch here... secDB should trump in this case
- VersionFormat: "apk",
+ nvdVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("> 0.9.0, < 0.10.0", version.UnknownFormat), // note: this is not normal NVD configuration, but has the desired effect of a "wide net" for vulnerable indication
+ CPEs: []cpe.CPE{
+ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""),
+ },
}
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "nvd:cpe": {
- "libvncserver": []grypeDB.Vulnerability{nvdVuln},
- },
- "secdb:distro:alpine:3.12": {
- "libvncserver": []grypeDB.Vulnerability{secDbVuln},
- },
+ secDbVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "secdb:distro:alpine:3.12",
},
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("< 0.9.11", version.ApkFormat), // note: this does NOT include 0.9.11, so NVD and SecDB mismatch here... secDB should trump in this case
}
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
+ vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)
m := Matcher{}
d, err := distro.New(distro.Alpine, "3.12.0", "")
@@ -490,48 +574,43 @@ func TestNvdMatchesWithSecDBFix(t *testing.T) {
Name: "libvncserver",
Version: "0.9.11",
Type: syftPkg.ApkPkg,
+ Distro: d,
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""),
},
}
- expected := []match.Match{}
+ var expected []match.Match
- actual, err := m.Match(provider, d, p)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
assertMatches(t, expected, actual)
}
func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) {
- nvdVuln := grypeDB.Vulnerability{
- ID: "CVE-2020-1",
- VersionConstraint: "", // note: empty value indicates that all versions are vulnerable
- VersionFormat: "unknown",
- CPEs: []string{`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`},
- Namespace: "nvd:cpe",
- }
-
- secDbVuln := grypeDB.Vulnerability{
- ID: "CVE-2020-1",
- VersionConstraint: "< 0.9.11",
- VersionFormat: "apk",
- Namespace: "secdb:distro:alpine:3.12",
+ nvdVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("", version.UnknownFormat), // note: empty value indicates that all versions are vulnerable
+ CPEs: []cpe.CPE{
+ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""),
+ },
}
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "nvd:cpe": {
- "libvncserver": []grypeDB.Vulnerability{nvdVuln},
- },
- "secdb:distro:alpine:3.12": {
- "libvncserver": []grypeDB.Vulnerability{secDbVuln},
- },
+ secDbVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "secdb:distro:alpine:3.12",
},
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("< 0.9.11", version.ApkFormat),
}
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
+ vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)
m := Matcher{}
d, err := distro.New(distro.Alpine, "3.12.0", "")
@@ -543,38 +622,84 @@ func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) {
Name: "libvncserver",
Version: "0.9.11",
Type: syftPkg.ApkPkg,
+ Distro: d,
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""),
},
}
- expected := []match.Match{}
+ var expected []match.Match
- actual, err := m.Match(provider, d, p)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
assertMatches(t, expected, actual)
}
-func TestDistroMatchBySourceIndirection(t *testing.T) {
+func TestNVDMatchCanceledByOriginPackageInSecDB(t *testing.T) {
+ nvdVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2015-3211",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "php-fpm",
+ Constraint: version.MustGetConstraint("", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:php-fpm:php-fpm:-:*:*:*:*:*:*:*", ""),
+ },
+ }
+ secDBVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2015-3211",
+ Namespace: "wolfi:distro:wolfi:rolling",
+ },
+ PackageName: "php-8.3",
+ Constraint: version.MustGetConstraint("< 0", version.ApkFormat),
+ }
+ vp := mock.VulnerabilityProvider(nvdVuln, secDBVuln)
- secDbVuln := grypeDB.Vulnerability{
- // ID doesn't match - this is the key for comparison in the matcher
- ID: "CVE-2020-2",
- VersionConstraint: "<= 1.3.3-r0",
- VersionFormat: "apk",
- Namespace: "secdb:distro:alpine:3.12",
- }
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "secdb:distro:alpine:3.12": {
- "musl": []grypeDB.Vulnerability{secDbVuln},
+ m := Matcher{}
+ d, err := distro.New(distro.Wolfi, "", "")
+ if err != nil {
+ t.Fatalf("failed to create a new distro: %+v", err)
+ }
+ p := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "php-8.3-fpm", // the package will not match anything
+ Version: "8.3.11-r0",
+ Type: syftPkg.ApkPkg,
+ Distro: d,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:php-fpm:php-fpm:8.3.11-r0:*:*:*:*:*:*:*", ""),
+ },
+ Upstreams: []pkg.UpstreamPackage{
+ {
+ Name: "php-8.3", // this upstream should match
+ Version: "8.3.11-r0",
},
},
}
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
+ var expected []match.Match
+
+ actual, _, err := m.Match(vp, p)
+ assert.NoError(t, err)
+
+ assertMatches(t, expected, actual)
+}
+
+func TestDistroMatchBySourceIndirection(t *testing.T) {
+
+ secDbVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ // ID doesn't match - this is the key for comparison in the matcher
+ ID: "CVE-2020-2",
+ Namespace: "secdb:distro:alpine:3.12",
+ },
+ PackageName: "musl",
+ Constraint: version.MustGetConstraint("<= 1.3.3-r0", version.ApkFormat),
+ }
+ vp := mock.VulnerabilityProvider(secDbVuln)
m := Matcher{}
d, err := distro.New(distro.Alpine, "3.12.0", "")
@@ -586,20 +711,92 @@ func TestDistroMatchBySourceIndirection(t *testing.T) {
Name: "musl-utils",
Version: "1.3.2-r0",
Type: syftPkg.ApkPkg,
+ Distro: d,
Upstreams: []pkg.UpstreamPackage{
{
Name: "musl",
},
},
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
+ },
+ }
+
+ expected := []match.Match{
+ {
+
+ Vulnerability: secDbVuln,
+ Package: p,
+ Details: []match.Detail{
+ {
+ Type: match.ExactIndirectMatch,
+ Confidence: 1.0,
+ SearchedBy: map[string]interface{}{
+ "distro": map[string]string{
+ "type": d.Type.String(),
+ "version": d.Version,
+ },
+ "package": map[string]string{
+ "name": "musl",
+ "version": p.Version,
+ },
+ "namespace": "secdb:distro:alpine:3.12",
+ },
+ Found: map[string]interface{}{
+ "versionConstraint": secDbVuln.Constraint.String(),
+ "vulnerabilityID": "CVE-2020-2",
+ },
+ Matcher: match.ApkMatcher,
+ },
+ },
+ },
}
- vulnFound, err := vulnerability.NewVulnerability(secDbVuln)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
+ assertMatches(t, expected, actual)
+}
+
+func TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) {
+ // this should match the test package
+ // the test package will have no CPE causing an error,
+ // but the error should not cause the secDB matches to fail
+ secDbVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-2",
+ Namespace: "secdb:distro:alpine:3.12",
+ },
+ PackageName: "musl",
+ Constraint: version.MustGetConstraint("<= 1.3.3-r0", version.ApkFormat),
+ }
+
+ vp := mock.VulnerabilityProvider(secDbVuln)
+
+ m := Matcher{}
+ d, err := distro.New(distro.Alpine, "3.12.0", "")
+ if err != nil {
+ t.Fatalf("failed to create a new distro: %+v", err)
+ }
+
+ p := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "musl-utils",
+ Version: "1.3.2-r0",
+ Type: syftPkg.ApkPkg,
+ Distro: d,
+ Upstreams: []pkg.UpstreamPackage{
+ {
+ Name: "musl",
+ },
+ },
+ CPEs: []cpe.CPE{},
+ }
+
expected := []match.Match{
{
- Vulnerability: *vulnFound,
+ Vulnerability: secDbVuln,
Package: p,
Details: []match.Detail{
{
@@ -608,7 +805,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",
@@ -617,7 +814,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) {
"namespace": "secdb:distro:alpine:3.12",
},
Found: map[string]interface{}{
- "versionConstraint": vulnFound.Constraint.String(),
+ "versionConstraint": secDbVuln.Constraint.String(),
"vulnerabilityID": "CVE-2020-2",
},
Matcher: match.ApkMatcher,
@@ -626,30 +823,25 @@ func TestDistroMatchBySourceIndirection(t *testing.T) {
},
}
- actual, err := m.Match(provider, d, p)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
assertMatches(t, expected, actual)
}
func TestNVDMatchBySourceIndirection(t *testing.T) {
- nvdVuln := grypeDB.Vulnerability{
- ID: "CVE-2020-1",
- VersionConstraint: "<= 1.3.3-r0",
- VersionFormat: "unknown",
- CPEs: []string{"cpe:2.3:a:musl:musl:*:*:*:*:*:*:*:*"},
- Namespace: "nvd:cpe",
- }
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "nvd:cpe": {
- "musl": []grypeDB.Vulnerability{nvdVuln},
- },
+ nvdVuln := vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "musl",
+ Constraint: version.MustGetConstraint("<= 1.3.3-r0", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:musl:musl:*:*:*:*:*:*:*:*", ""),
},
}
-
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
+ vp := mock.VulnerabilityProvider(nvdVuln)
m := Matcher{}
d, err := distro.New(distro.Alpine, "3.12.0", "")
@@ -661,9 +853,10 @@ func TestNVDMatchBySourceIndirection(t *testing.T) {
Name: "musl-utils",
Version: "1.3.2-r0",
Type: syftPkg.ApkPkg,
+ Distro: d,
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*"),
- cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*", ""),
+ cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*", ""),
},
Upstreams: []pkg.UpstreamPackage{
{
@@ -672,29 +865,25 @@ func TestNVDMatchBySourceIndirection(t *testing.T) {
},
}
- vulnFound, err := vulnerability.NewVulnerability(nvdVuln)
- assert.NoError(t, err)
- vulnFound.CPEs = []cpe.CPE{cpe.Must(nvdVuln.CPEs[0])}
-
expected := []match.Match{
{
- Vulnerability: *vulnFound,
+ Vulnerability: nvdVuln,
Package: p,
Details: []match.Detail{
{
Type: match.CPEMatch,
Confidence: 0.9,
- SearchedBy: search.CPEParameters{
- CPEs: []string{"cpe:2.3:a:musl:musl:*:*:*:*:*:*:*:*"},
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:a:musl:musl:1.3.2-r0:*:*:*:*:*:*:*"},
Namespace: "nvd:cpe",
- Package: search.CPEPackageParameter{
+ Package: match.CPEPackageParameter{
Name: "musl",
Version: "1.3.2-r0",
},
},
- Found: search.CPEResult{
- CPEs: []string{vulnFound.CPEs[0].BindToFmtString()},
- VersionConstraint: vulnFound.Constraint.String(),
+ Found: match.CPEResult{
+ CPEs: []string{nvdVuln.CPEs[0].Attributes.BindToFmtString()},
+ VersionConstraint: nvdVuln.Constraint.String(),
VulnerabilityID: "CVE-2020-1",
},
Matcher: match.ApkMatcher,
@@ -703,7 +892,7 @@ func TestNVDMatchBySourceIndirection(t *testing.T) {
},
}
- actual, err := m.Match(provider, d, p)
+ actual, _, err := m.Match(vp, p)
assert.NoError(t, err)
assertMatches(t, expected, actual)
@@ -714,9 +903,53 @@ 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 != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
+
+func Test_nakConstraint(t *testing.T) {
+ tests := []struct {
+ name string
+ input vulnerability.Vulnerability
+ wantErr require.ErrorAssertionFunc
+ matches bool
+ }{
+ {
+ name: "matches apk",
+ input: vulnerability.Vulnerability{
+ Constraint: version.MustGetConstraint("< 0", version.ApkFormat),
+ },
+ matches: true,
+ },
+ {
+ name: "not match due to type",
+ input: vulnerability.Vulnerability{
+ Constraint: version.MustGetConstraint("< 0", version.SemanticFormat),
+ },
+ matches: false,
+ },
+ {
+ name: "not match",
+ input: vulnerability.Vulnerability{
+ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat),
+ },
+ matches: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ matches, _, err := nakConstraint.MatchesVulnerability(tt.input)
+ wantErr := require.NoError
+ if tt.wantErr != nil {
+ wantErr = tt.wantErr
+ }
+ wantErr(t, err)
+ require.Equal(t, tt.matches, matches)
+ })
+ }
+}
diff --git a/grype/matcher/dotnet/matcher.go b/grype/matcher/dotnet/matcher.go
index ef2af39815d..a78c1d11ccb 100644
--- a/grype/matcher/dotnet/matcher.go
+++ b/grype/matcher/dotnet/matcher.go
@@ -1,10 +1,9 @@
package dotnet
import (
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType {
return match.DotnetMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
- criteria := search.CommonCriteria
- if m.cfg.UseCPEs {
- criteria = append(criteria, search.ByCPE)
- }
- return search.ByCriteria(store, d, p, m.Type(), criteria...)
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
+ return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)
}
diff --git a/grype/matcher/dpkg/matcher.go b/grype/matcher/dpkg/matcher.go
index ca94d28c7b7..3470661a827 100644
--- a/grype/matcher/dpkg/matcher.go
+++ b/grype/matcher/dpkg/matcher.go
@@ -3,10 +3,9 @@ package dpkg
import (
"fmt"
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -22,29 +21,29 @@ func (m *Matcher) Type() match.MatcherType {
return match.DpkgMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
matches := make([]match.Match, 0)
- sourceMatches, err := m.matchUpstreamPackages(store, d, p)
+ sourceMatches, err := m.matchUpstreamPackages(store, p)
if err != nil {
- return nil, fmt.Errorf("failed to match by source indirection: %w", err)
+ return nil, nil, fmt.Errorf("failed to match by source indirection: %w", err)
}
matches = append(matches, sourceMatches...)
- exactMatches, err := search.ByPackageDistro(store, d, p, m.Type())
+ exactMatches, _, err := internal.MatchPackageByDistro(store, p, m.Type())
if err != nil {
- return nil, fmt.Errorf("failed to match by exact package name: %w", err)
+ return nil, nil, fmt.Errorf("failed to match by exact package name: %w", err)
}
matches = append(matches, exactMatches...)
- return matches, nil
+ return matches, nil, nil
}
-func (m *Matcher) matchUpstreamPackages(store vulnerability.ProviderByDistro, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) matchUpstreamPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) {
var matches []match.Match
for _, indirectPackage := range pkg.UpstreamPackages(p) {
- indirectMatches, err := search.ByPackageDistro(store, d, indirectPackage, m.Type())
+ indirectMatches, _, err := internal.MatchPackageByDistro(store, indirectPackage, m.Type())
if err != nil {
return nil, fmt.Errorf("failed to find vulnerabilities for dpkg upstream source package: %w", err)
}
diff --git a/grype/matcher/dpkg/matcher_mocks_test.go b/grype/matcher/dpkg/matcher_mocks_test.go
index a315ad0de1b..daa5b9aa7bb 100644
--- a/grype/matcher/dpkg/matcher_mocks_test.go
+++ b/grype/matcher/dpkg/matcher_mocks_test.go
@@ -1,55 +1,34 @@
package dpkg
import (
- "strings"
-
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
)
-type mockProvider struct {
- data map[string]map[string][]vulnerability.Vulnerability
-}
-
-func newMockProvider() *mockProvider {
- pr := mockProvider{
- data: make(map[string]map[string][]vulnerability.Vulnerability),
- }
- pr.stub()
- return &pr
-}
-
-func (pr *mockProvider) stub() {
- pr.data["debian:8"] = map[string][]vulnerability.Vulnerability{
- // direct...
- "neutron": {
- {
- Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
- ID: "CVE-2014-fake-1",
- },
+func newMockProvider() vulnerability.Provider {
+ return mock.VulnerabilityProvider([]vulnerability.Vulnerability{
+ {
+ PackageName: "neutron",
+ Reference: vulnerability.Reference{ID: "CVE-2014-fake-1", Namespace: "secdb:distro:debian:8"},
+ Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
},
- // indirect...
- "neutron-devel": {
- // expected...
- {
- Constraint: version.MustGetConstraint("< 2014.1.4-5", version.DebFormat),
- ID: "CVE-2014-fake-2",
- },
- {
- Constraint: version.MustGetConstraint("< 2015.0.0-1", version.DebFormat),
- ID: "CVE-2013-fake-3",
- },
- // unexpected...
- {
- Constraint: version.MustGetConstraint("< 2014.0.4-1", version.DebFormat),
- ID: "CVE-2013-fake-BAD",
- },
+ // expected...
+ {
+ PackageName: "neutron-devel",
+ Constraint: version.MustGetConstraint("< 2014.1.4-5", version.DebFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "secdb:distro:debian:8"},
},
- }
-}
-
-func (pr *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- return pr.data[strings.ToLower(d.Type.String())+":"+d.FullVersion()][p.Name], nil
+ {
+ PackageName: "neutron-devel",
+ Constraint: version.MustGetConstraint("< 2015.0.0-1", version.DebFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: "secdb:distro:debian:8"},
+ },
+ // unexpected...
+ {
+ PackageName: "neutron-devel",
+ Constraint: version.MustGetConstraint("< 2014.0.4-1", version.DebFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: "secdb:distro:debian:8"},
+ },
+ }...)
}
diff --git a/grype/matcher/dpkg/matcher_test.go b/grype/matcher/dpkg/matcher_test.go
index b04a5c477de..fbbd80ff009 100644
--- a/grype/matcher/dpkg/matcher_test.go
+++ b/grype/matcher/dpkg/matcher_test.go
@@ -16,11 +16,18 @@ import (
func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) {
matcher := Matcher{}
+
+ d, err := distro.New(distro.Debian, "8", "")
+ if err != nil {
+ t.Fatal("could not create distro: ", err)
+ }
+
p := pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "neutron",
Version: "2014.1.3-6",
Type: syftPkg.DebPkg,
+ Distro: d,
Upstreams: []pkg.UpstreamPackage{
{
Name: "neutron-devel",
@@ -28,13 +35,8 @@ func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) {
},
}
- d, err := distro.New(distro.Debian, "8", "")
- if err != nil {
- t.Fatal("could not create distro: ", err)
- }
-
- store := newMockProvider()
- actual, err := matcher.matchUpstreamPackages(store, d, p)
+ vp := newMockProvider()
+ actual, err := matcher.matchUpstreamPackages(vp, p)
assert.NoError(t, err, "unexpected err from matchUpstreamPackages", err)
assert.Len(t, actual, 2, "unexpected indirect matches count")
diff --git a/grype/matcher/golang/matcher.go b/grype/matcher/golang/matcher.go
index 08d5b59c509..bdb014ca9b8 100644
--- a/grype/matcher/golang/matcher.go
+++ b/grype/matcher/golang/matcher.go
@@ -3,10 +3,9 @@ package golang
import (
"strings"
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -16,7 +15,9 @@ type Matcher struct {
}
type MatcherConfig struct {
- UseCPEs bool
+ UseCPEs bool
+ AlwaysUseCPEForStdlib bool
+ AllowMainModulePseudoVersionComparison bool
}
func NewGolangMatcher(cfg MatcherConfig) *Matcher {
@@ -33,7 +34,7 @@ func (m *Matcher) Type() match.MatcherType {
return match.GoModuleMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
matches := make([]match.Match, 0)
mainModule := ""
@@ -41,18 +42,31 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa
mainModule = m.MainModule
}
- // Golang currently does not have a standard way of incorporating the vcs version
- // into the compiled binary: https://github.com/golang/go/issues/50603
- // current version information for the main module is incomplete leading to multiple FP
- // TODO: remove this exclusion when vcs information is included in future go version
- isNotCorrected := strings.HasPrefix(p.Version, "v0.0.0-") || strings.HasPrefix(p.Version, "(devel)")
+ // Golang currently does not have a standard way of incorporating the main
+ // module's version into the compiled binary:
+ // https://github.com/golang/go/issues/50603.
+ //
+ // Syft has some fallback mechanisms to come up with a more sane version value
+ // depending on the scenario. But if none of these apply, the Go-set value of
+ // "(devel)" is used, which is altogether unhelpful for vulnerability matching.
+ var isNotCorrected bool
+ if m.cfg.AllowMainModulePseudoVersionComparison {
+ isNotCorrected = strings.HasPrefix(p.Version, "(devel)")
+ } else {
+ // when AllowPseudoVersionComparison is false
+ isNotCorrected = strings.HasPrefix(p.Version, "v0.0.0-") || strings.HasPrefix(p.Version, "(devel)")
+ }
if p.Name == mainModule && isNotCorrected {
- return matches, nil
+ return matches, nil, nil
}
- criteria := search.CommonCriteria
- if m.cfg.UseCPEs {
- criteria = append(criteria, search.ByCPE)
+ return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), searchByCPE(p.Name, m.cfg))
+}
+
+func searchByCPE(name string, cfg MatcherConfig) bool {
+ if cfg.UseCPEs {
+ return true
}
- return search.ByCriteria(store, d, p, m.Type(), criteria...)
+
+ return cfg.AlwaysUseCPEForStdlib && (name == "stdlib")
}
diff --git a/grype/matcher/golang/matcher_test.go b/grype/matcher/golang/matcher_test.go
index 0efe4f2862d..81206078b0c 100644
--- a/grype/matcher/golang/matcher_test.go
+++ b/grype/matcher/golang/matcher_test.go
@@ -4,73 +4,208 @@ import (
"testing"
"github.com/google/uuid"
+ "github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
"github.com/anchore/syft/syft/cpe"
syftPkg "github.com/anchore/syft/syft/pkg"
)
-func TestMatcherGolang_DropMainPackage(t *testing.T) {
- p := pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "istio.io/istio",
- Version: "v0.0.0-20220606222826-f59ce19ec6b6",
- Type: syftPkg.GoModulePkg,
- MetadataType: pkg.GolangBinMetadataType,
- Metadata: pkg.GolangBinMetadata{
- MainModule: "istio.io/istio",
+func TestMatcher_DropMainPackageGivenVersionInfo(t *testing.T) {
+ tests := []struct {
+ name string
+ subjectWithoutMainModule pkg.Package
+ mainModuleData pkg.GolangBinMetadata
+ allowPsuedoVersionComparison bool
+ expectedMatchCount int
+ }{
+ {
+ name: "main module with version is matched when pseudo version comparison is allowed",
+ subjectWithoutMainModule: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "istio.io/istio",
+ Version: "v0.0.0-20220606222826-f59ce19ec6b6",
+ Type: syftPkg.GoModulePkg,
+ Language: syftPkg.Go,
+ Metadata: pkg.GolangBinMetadata{},
+ },
+ mainModuleData: pkg.GolangBinMetadata{
+ MainModule: "istio.io/istio",
+ },
+ allowPsuedoVersionComparison: true,
+ expectedMatchCount: 1,
+ },
+ {
+ name: "main module with version is NOT matched when pseudo version comparison is disabled",
+ subjectWithoutMainModule: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "istio.io/istio",
+ Version: "v0.0.0-20220606222826-f59ce19ec6b6",
+ Type: syftPkg.GoModulePkg,
+ Language: syftPkg.Go,
+ Metadata: pkg.GolangBinMetadata{},
+ },
+ mainModuleData: pkg.GolangBinMetadata{
+ MainModule: "istio.io/istio",
+ },
+ allowPsuedoVersionComparison: false,
+ expectedMatchCount: 0,
},
}
- matcher := Matcher{}
- store := newMockProvider()
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ mainModuleMetadata := test.mainModuleData
+ subjectWithoutMainModule := test.subjectWithoutMainModule
- actual, _ := matcher.Match(store, nil, p)
- assert.Len(t, actual, 0, "unexpected match count; should not match main module")
-}
+ subjectWithMainModule := subjectWithoutMainModule
+ subjectWithMainModule.Metadata = mainModuleMetadata
-func newMockProvider() *mockProvider {
- mp := mockProvider{
- data: make(map[syftPkg.Language]map[string][]vulnerability.Vulnerability),
- }
+ subjectWithMainModuleAsDevel := subjectWithMainModule
+ subjectWithMainModuleAsDevel.Version = "(devel)"
- mp.populateData()
+ matcher := NewGolangMatcher(MatcherConfig{
+ AllowMainModulePseudoVersionComparison: test.allowPsuedoVersionComparison,
+ })
+ store := newMockProvider()
- return &mp
-}
+ preTest, _, _ := matcher.Match(store, subjectWithoutMainModule)
+ assert.Len(t, preTest, 1, "should have matched the package when there is not a main module")
-type mockProvider struct {
- data map[syftPkg.Language]map[string][]vulnerability.Vulnerability
-}
+ actual, _, _ := matcher.Match(store, subjectWithMainModule)
+ assert.Len(t, actual, test.expectedMatchCount, "should match the main module depending on config (i.e. 1 match)")
-func (mp *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
- //TODO implement me
- panic("implement me")
+ actual, _, _ = matcher.Match(store, subjectWithMainModuleAsDevel)
+ assert.Len(t, actual, 0, "unexpected match count; should never match main module (devel)")
+ })
+ }
}
-func (mp *mockProvider) populateData() {
- mp.data[syftPkg.Go] = map[string][]vulnerability.Vulnerability{
- "istio.io/istio": {
- {
- Constraint: version.MustGetConstraint("<5.0.7", version.UnknownFormat),
- ID: "CVE-2013-fake-BAD",
+func TestMatcher_SearchForStdlib(t *testing.T) {
+
+ // values derived from:
+ // $ go version -m $(which grype)
+ // /opt/homebrew/bin/grype: go1.21.1
+
+ subject := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "stdlib",
+ Version: "go1.18.3",
+ Type: syftPkg.GoModulePkg,
+ Language: syftPkg.Go,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:golang:go:1.18.3:-:*:*:*:*:*:*", ""),
+ },
+ Metadata: pkg.GolangBinMetadata{},
+ }
+
+ cases := []struct {
+ name string
+ cfg MatcherConfig
+ subject pkg.Package
+ expectedCVEs []string
+ }{
+ // positive
+ {
+ name: "cpe enables, no override enabled",
+ cfg: MatcherConfig{
+ UseCPEs: true,
+ AlwaysUseCPEForStdlib: false,
+ },
+ subject: subject,
+ expectedCVEs: []string{
+ "CVE-2022-27664",
+ },
+ },
+ {
+ name: "stdlib search, cpe enables, no override enabled",
+ cfg: MatcherConfig{
+ UseCPEs: true,
+ AlwaysUseCPEForStdlib: true,
},
+ subject: subject,
+ expectedCVEs: []string{
+ "CVE-2022-27664",
+ },
+ },
+ {
+ name: "stdlib search, cpe enables, no override enabled",
+ cfg: MatcherConfig{
+ UseCPEs: false,
+ AlwaysUseCPEForStdlib: true,
+ },
+ subject: subject,
+ expectedCVEs: []string{
+ "CVE-2022-27664",
+ },
+ },
+ {
+ name: "go package search should be found by cpe",
+ cfg: MatcherConfig{
+ UseCPEs: true,
+ AlwaysUseCPEForStdlib: true,
+ },
+ subject: func() pkg.Package { p := subject; p.Name = "go"; return p }(),
+ expectedCVEs: []string{
+ "CVE-2022-27664",
+ }},
+ // negative
+ {
+ name: "stdlib search, cpe suppressed, no override enabled",
+ cfg: MatcherConfig{
+ UseCPEs: false,
+ AlwaysUseCPEForStdlib: false,
+ },
+ subject: subject,
+ expectedCVEs: nil,
+ },
+ {
+ name: "go package search should not be an exception (only the stdlib)",
+ cfg: MatcherConfig{
+ UseCPEs: false,
+ AlwaysUseCPEForStdlib: true,
+ },
+ subject: func() pkg.Package { p := subject; p.Name = "go"; return p }(),
+ expectedCVEs: nil,
},
}
-}
-func (mp *mockProvider) GetByCPE(p cpe.CPE) ([]vulnerability.Vulnerability, error) {
- return []vulnerability.Vulnerability{}, nil
-}
+ store := newMockProvider()
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ matcher := NewGolangMatcher(c.cfg)
+
+ actual, _, _ := matcher.Match(store, c.subject)
+ actualCVEs := strset.New()
+ for _, m := range actual {
+ actualCVEs.Add(m.Vulnerability.ID)
+ }
+
+ expectedCVEs := strset.New(c.expectedCVEs...)
-func (mp *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- return []vulnerability.Vulnerability{}, nil
+ assert.ElementsMatch(t, expectedCVEs.List(), actualCVEs.List())
+
+ })
+ }
}
-func (mp *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- return mp.data[l][p.Name], nil
+func newMockProvider() vulnerability.Provider {
+ return mock.VulnerabilityProvider([]vulnerability.Vulnerability{
+ // for TestMatcher_DropMainPackageIfNoVersion
+ {
+ PackageName: "istio.io/istio",
+ Constraint: version.MustGetConstraint("< 5.0.7", version.UnknownFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: "github:language:" + syftPkg.Go.String()},
+ },
+ {
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:golang:go:1.18.3:-:*:*:*:*:*:*", "test")},
+ Constraint: version.MustGetConstraint("< 1.18.6 || = 1.19.0", version.UnknownFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2022-27664", Namespace: "nvd:cpe"},
+ },
+ }...)
}
diff --git a/grype/matcher/internal/common.go b/grype/matcher/internal/common.go
new file mode 100644
index 00000000000..b6b5e2e223b
--- /dev/null
+++ b/grype/matcher/internal/common.go
@@ -0,0 +1,43 @@
+package internal
+
+import (
+ "errors"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+)
+
+func MatchPackageByEcosystemAndCPEs(store vulnerability.Provider, p pkg.Package, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoredMatch, error) {
+ var matches []match.Match
+ var ignored []match.IgnoredMatch
+
+ for _, name := range store.PackageSearchNames(p) {
+ nameMatches, nameIgnores, err := MatchPackageByEcosystemPackageNameAndCPEs(store, p, name, matcher, includeCPEs)
+ if err != nil {
+ return nil, nil, err
+ }
+ matches = append(matches, nameMatches...)
+ ignored = append(ignored, nameIgnores...)
+ }
+
+ return matches, ignored, nil
+}
+
+func MatchPackageByEcosystemPackageNameAndCPEs(store vulnerability.Provider, p pkg.Package, packageName string, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoredMatch, error) {
+ matches, ignored, err := MatchPackageByEcosystemPackageName(store, p, packageName, matcher)
+ if err != nil {
+ log.Debugf("could not match by package ecosystem (package=%+v): %v", p, err)
+ }
+ if includeCPEs {
+ cpeMatches, err := MatchPackageByCPEs(store, p, matcher)
+ if errors.Is(err, ErrEmptyCPEMatch) {
+ log.Debugf("attempted CPE search on %s, which has no CPEs. Consider re-running with --add-cpes-if-none", p.Name)
+ } else if err != nil {
+ log.Debugf("could not match by package CPE (package=%+v): %v", p, err)
+ }
+ matches = append(matches, cpeMatches...)
+ }
+ return matches, ignored, nil
+}
diff --git a/grype/matcher/internal/cpe.go b/grype/matcher/internal/cpe.go
new file mode 100644
index 00000000000..0af04b69561
--- /dev/null
+++ b/grype/matcher/internal/cpe.go
@@ -0,0 +1,245 @@
+package internal
+
+import (
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/facebookincubator/nvdtools/wfn"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/syft/syft/cpe"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func alpineCPEComparableVersion(version string) string {
+ // clean the alpine package version so that it compares correctly with the CPE version comparison logic
+ // alpine versions are suffixed with -r{buildindex}; however, if left intact CPE comparison logic will
+ // incorrectly treat these as a pre-release. In actuality, we just want to treat 1.2.3-r21 as equivalent to
+ // 1.2.3 for purposes of CPE-based matching since the alpine fix should filter out any cases where a later
+ // build fixes something that was vulnerable in 1.2.3
+ components := strings.Split(version, "-r")
+ cpeComparableVersion := version
+
+ if len(components) == 2 {
+ cpeComparableVersion = components[0]
+ }
+
+ return cpeComparableVersion
+}
+
+var ErrEmptyCPEMatch = errors.New("attempted CPE match against package with no CPEs")
+
+// MatchPackageByCPEs retrieves all vulnerabilities that match any of the provided package's CPEs
+func MatchPackageByCPEs(provider vulnerability.Provider, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) {
+ // we attempt to merge match details within the same matcher when searching by CPEs, in this way there are fewer duplicated match
+ // objects (and fewer duplicated match details).
+
+ // Warn the user if they are matching by CPE, but there are no CPEs available.
+ if len(p.CPEs) == 0 {
+ return nil, ErrEmptyCPEMatch
+ }
+
+ matchesByFingerprint := make(map[match.Fingerprint]match.Match)
+ for _, c := range p.CPEs {
+ // prefer the CPE version, but if npt specified use the package version
+ searchVersion := c.Attributes.Version
+
+ if p.Type == syftPkg.ApkPkg {
+ searchVersion = alpineCPEComparableVersion(searchVersion)
+ }
+
+ if searchVersion == wfn.NA || searchVersion == wfn.Any || isUnknownVersion(searchVersion) {
+ searchVersion = p.Version
+ }
+
+ if isUnknownVersion(searchVersion) {
+ log.WithFields("package", p.Name).Trace("skipping package with unknown version")
+ continue
+ }
+
+ // we should always show the exact CPE we searched by, not just what's in the component analysis (since we
+ // may alter the version based on above processing)
+ c.Attributes.Version = searchVersion
+
+ format := version.FormatFromPkg(p)
+
+ if format == version.JVMFormat {
+ searchVersion = transformJvmVersion(searchVersion, c.Attributes.Update)
+ }
+
+ 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)
+ vulns, err := provider.FindVulnerabilities(
+ search.ByCPE(c),
+ onlyVulnerableTargets(p),
+ onlyQualifiedPackages(p),
+ onlyVulnerableVersions(verObj),
+ onlyNonWithdrawnVulnerabilities(),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("matcher failed to fetch by CPE pkg=%q: %w", p.Name, err)
+ }
+
+ // for each vulnerability record found, check the version constraint. If the constraint is satisfied
+ // 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)
+ }
+ }
+
+ return toMatches(matchesByFingerprint), nil
+}
+
+func transformJvmVersion(searchVersion, updateCpeField string) string {
+ // we should take into consideration the CPE update field for JVM packages
+ if strings.HasPrefix(searchVersion, "1.") && !strings.Contains(searchVersion, "_") && updateCpeField != wfn.NA && updateCpeField != wfn.Any {
+ searchVersion = fmt.Sprintf("%s_%s", searchVersion, strings.TrimPrefix(updateCpeField, "update"))
+ }
+ 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) {
+ candidateMatch := match.Match{
+
+ Vulnerability: vuln,
+ Package: p,
+ }
+
+ if existingMatch, exists := matchesByFingerprint[candidateMatch.Fingerprint()]; exists {
+ candidateMatch = existingMatch
+ }
+
+ candidateMatch.Details = addMatchDetails(candidateMatch.Details,
+ match.Detail{
+ Type: match.CPEMatch,
+ Confidence: 0.9, // TODO: this is hard coded for now
+ Matcher: upstreamMatcher,
+ SearchedBy: match.CPEParameters{
+ Namespace: vuln.Namespace,
+ CPEs: []string{
+ searchedByCPE.Attributes.BindToFmtString(),
+ },
+ Package: match.CPEPackageParameter{
+ Name: p.Name,
+ Version: p.Version,
+ },
+ },
+ Found: match.CPEResult{
+ VulnerabilityID: vuln.ID,
+ VersionConstraint: vuln.Constraint.String(),
+ CPEs: cpesToString(filterCPEsByVersion(searchVersion, vuln.CPEs)),
+ },
+ },
+ )
+
+ matchesByFingerprint[candidateMatch.Fingerprint()] = candidateMatch
+}
+
+func addMatchDetails(existingDetails []match.Detail, newDetails match.Detail) []match.Detail {
+ newFound, ok := newDetails.Found.(match.CPEResult)
+ if !ok {
+ return existingDetails
+ }
+
+ newSearchedBy, ok := newDetails.SearchedBy.(match.CPEParameters)
+ if !ok {
+ return existingDetails
+ }
+ for idx, detail := range existingDetails {
+ found, ok := detail.Found.(match.CPEResult)
+ if !ok {
+ continue
+ }
+
+ searchedBy, ok := detail.SearchedBy.(match.CPEParameters)
+ if !ok {
+ continue
+ }
+
+ if !found.Equals(newFound) {
+ continue
+ }
+
+ err := searchedBy.Merge(newSearchedBy)
+ if err != nil {
+ continue
+ }
+
+ existingDetails[idx].SearchedBy = searchedBy
+ return existingDetails
+ }
+
+ // could not merge with another entry, append to the end
+ existingDetails = append(existingDetails, newDetails)
+ return existingDetails
+}
+
+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)
+ continue
+ }
+
+ ver := c.Attributes.Version
+
+ if pkgVersion.Format == version.JVMFormat {
+ if c.Attributes.Update != wfn.Any && c.Attributes.Update != wfn.NA {
+ ver = transformJvmVersion(ver, c.Attributes.Update)
+ }
+ }
+
+ constraint, err := version.GetConstraint(ver, pkgVersion.Format)
+ if err != nil {
+ // if we can't get a version constraint, don't filter out the CPE
+ matchedCPEs = append(matchedCPEs, c)
+ continue
+ }
+
+ 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)
+ continue
+ }
+ }
+ return matchedCPEs
+}
+
+func toMatches(matchesByFingerprint map[match.Fingerprint]match.Match) (matches []match.Match) {
+ for _, m := range matchesByFingerprint {
+ matches = append(matches, m)
+ }
+ sort.Sort(match.ByElements(matches))
+ return matches
+}
+
+// cpesToString receives one or more CPEs and stringifies them
+func cpesToString(cpes []cpe.CPE) []string {
+ var strs = make([]string, len(cpes))
+ for idx, c := range cpes {
+ strs[idx] = c.Attributes.BindToFmtString()
+ }
+ sort.Strings(strs)
+ return strs
+}
diff --git a/grype/matcher/internal/cpe_test.go b/grype/matcher/internal/cpe_test.go
new file mode 100644
index 00000000000..e61d44de7dc
--- /dev/null
+++ b/grype/matcher/internal/cpe_test.go
@@ -0,0 +1,1445 @@
+package internal
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
+ "github.com/anchore/syft/syft/cpe"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func newCPETestStore() vulnerability.Provider {
+ return mock.VulnerabilityProvider([]vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.SemanticFormat),
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", "")},
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-2",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.4", version.SemanticFormat),
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*", "")},
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-3",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("= 4.0.1", version.GemFormat),
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*", "")},
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-4",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "awesome",
+ Constraint: version.MustGetConstraint("< 98SP3", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", ""),
+ },
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-5",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "multiple",
+ Constraint: version.MustGetConstraint("< 4.0", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", ""),
+ cpe.Must("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", ""),
+ cpe.Must("cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*", ""),
+ cpe.Must("cpe:2.3:*:multiple:multiple:3.0:*:*:*:*:*:*:*", ""),
+ },
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-6",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "funfun",
+ Constraint: version.MustGetConstraint("= 5.2.1", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*", ""),
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", ""),
+ },
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-7",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "sw",
+ Constraint: version.MustGetConstraint("< 1.0", version.UnknownFormat),
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*", "")},
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2021-23369",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "handlebars",
+ Constraint: version.MustGetConstraint("< 4.7.7", version.UnknownFormat),
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", "")},
+ },
+ }...)
+}
+
+func TestFindMatchesByPackageCPE(t *testing.T) {
+ matcher := match.RubyGemMatcher
+ tests := []struct {
+ name string
+ p pkg.Package
+ expected []match.Match
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "match from range",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando1:*:ra:*:ruby:*:*", ""),
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*", ""),
+ },
+ Name: "activerecord",
+ Version: "3.7.5",
+ 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:3.7.5:rando1:*:ra:*:ruby:*:*", ""),
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*", ""),
+ },
+ Name: "activerecord",
+ Version: "3.7.5",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"},
+ Package: match.CPEPackageParameter{
+ Name: "activerecord",
+ Version: "3.7.5",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
+ VersionConstraint: "< 3.7.6 (semver)",
+ VulnerabilityID: "CVE-2017-fake-1",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "fallback to package 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:*:*", ""),
+ },
+ Name: "activerecord",
+ Version: "3.7.5",
+ 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:unknown:rando1:*:ra:*:ruby:*:*", ""),
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:unknown:rando4:*:re:*:rails:*:*", ""),
+ },
+ Name: "activerecord",
+ Version: "3.7.5",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"},
+ Package: match.CPEPackageParameter{
+ Name: "activerecord",
+ Version: "3.7.5",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
+ VersionConstraint: "< 3.7.6 (semver)",
+ VulnerabilityID: "CVE-2017-fake-1",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "return all possible matches when missing version",
+ 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: "",
+ 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{},
+ },
+ {
+ name: "multiple matches",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*", ""),
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*", ""),
+ },
+ Name: "activerecord",
+ Version: "3.7.3",
+ 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:3.7.3:rando1:*:ra:*:ruby:*:*", ""),
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*", ""),
+ },
+ Name: "activerecord",
+ Version: "3.7.3",
+ 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:3.7.3:rando4:*:re:*:rails:*:*",
+ },
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "activerecord",
+ Version: "3.7.3",
+ },
+ },
+ 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:3.7.3:rando1:*:ra:*:ruby:*:*", ""),
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*", ""),
+ },
+ Name: "activerecord",
+ Version: "3.7.3",
+ 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:3.7.3:rando1:*:ra:*:ruby:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "activerecord",
+ Version: "3.7.3",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"},
+ VersionConstraint: "< 3.7.4 (semver)",
+ VulnerabilityID: "CVE-2017-fake-2",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "exact match",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*", ""),
+ },
+ Name: "activerecord",
+ Version: "4.0.1",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ },
+ expected: []match.Match{
+ {
+
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2017-fake-3"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*", ""),
+ },
+ Name: "activerecord",
+ Version: "4.0.1",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "activerecord",
+ Version: "4.0.1",
+ },
+ },
+ 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: "no match",
+ p: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "couldntgetthisrightcouldyou",
+ Version: "4.0.1",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:no_match:no_match:0.9.9:*:*:*:*:*:*:*", cpe.GeneratedSource),
+ },
+ },
+ expected: []match.Match{},
+ },
+ {
+ name: "fuzzy version match",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*", ""),
+ },
+ Name: "awesome",
+ Version: "98SE1",
+ },
+ expected: []match.Match{
+ {
+
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2017-fake-4"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*", ""),
+ },
+ Name: "awesome",
+ Version: "98SE1",
+ },
+
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "awesome",
+ Version: "98SE1",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*"},
+ VersionConstraint: "< 98SP3 (unknown)",
+ VulnerabilityID: "CVE-2017-fake-4",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "multiple matched CPEs",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", ""),
+ },
+ Name: "multiple",
+ Version: "1.0",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ },
+ expected: []match.Match{
+ {
+
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2017-fake-5"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", ""),
+ },
+ Name: "multiple",
+ Version: "1.0",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ },
+
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "multiple",
+ Version: "1.0",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ VersionConstraint: "< 4.0 (unknown)",
+ VulnerabilityID: "CVE-2017-fake-5",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "filtered out match due to target_sw mismatch",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "funfun",
+ Version: "5.2.1",
+ Language: syftPkg.Rust, // this is identified as a rust package
+ Type: syftPkg.RustPkg,
+ },
+ expected: []match.Match{},
+ },
+ {
+ name: "target_sw mismatch with unsupported target_sw",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "sw",
+ Version: "0.1",
+ Language: syftPkg.Erlang,
+ Type: syftPkg.HexPkg,
+ },
+ expected: []match.Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2017-fake-7"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "sw",
+ Version: "0.1",
+ Language: syftPkg.Erlang,
+ Type: syftPkg.HexPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:*:sw:sw:0.1:*:*:*:*:*:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "sw",
+ Version: "0.1",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{
+ "cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*",
+ },
+ VersionConstraint: "< 1.0 (unknown)",
+ VulnerabilityID: "CVE-2017-fake-7",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "match included even though multiple cpes are mismatch",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rust:*:*", ""),
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rails:*:*", ""),
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:ruby:*:*", ""),
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", ""),
+ },
+ Name: "funfun",
+ Version: "5.2.1",
+ Language: syftPkg.Python,
+ Type: syftPkg.PythonPkg,
+ },
+ expected: []match.Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2017-fake-6"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rust:*:*", ""),
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rails:*:*", ""),
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:ruby:*:*", ""),
+ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", ""),
+ },
+ Name: "funfun",
+ Version: "5.2.1",
+ Language: syftPkg.Python,
+ Type: syftPkg.PythonPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "funfun",
+ Version: "5.2.1",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{
+ "cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*",
+ "cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*",
+ },
+ VersionConstraint: "= 5.2.1 (unknown)",
+ VulnerabilityID: "CVE-2017-fake-6",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Ensure target_sw mismatch does not apply to java packages",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.Java,
+ Type: syftPkg.JavaPkg,
+ },
+ expected: []match.Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2021-23369"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.Java,
+ Type: syftPkg.JavaPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "handlebars",
+ Version: "0.1",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{
+ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
+ },
+ VersionConstraint: "< 4.7.7 (unknown)",
+ VulnerabilityID: "CVE-2021-23369",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Ensure target_sw mismatch does not apply to java jenkins plugins packages",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.Java,
+ Type: syftPkg.JenkinsPluginPkg,
+ },
+ expected: []match.Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2021-23369"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.Java,
+ Type: syftPkg.JenkinsPluginPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "handlebars",
+ Version: "0.1",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{
+ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
+ },
+ VersionConstraint: "< 4.7.7 (unknown)",
+ VulnerabilityID: "CVE-2021-23369",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Ensure target_sw mismatch does not apply to binary packages",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.UnknownLanguage,
+ Type: syftPkg.BinaryPkg,
+ },
+ expected: []match.Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2021-23369"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.UnknownLanguage,
+ Type: syftPkg.BinaryPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "handlebars",
+ Version: "0.1",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{
+ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
+ },
+ VersionConstraint: "< 4.7.7 (unknown)",
+ VulnerabilityID: "CVE-2021-23369",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Ensure target_sw mismatch does not apply to unknown packages",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.UnknownLanguage,
+ Type: syftPkg.UnknownPkg,
+ },
+ expected: []match.Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2021-23369"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.UnknownLanguage,
+ Type: syftPkg.UnknownPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "handlebars",
+ Version: "0.1",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{
+ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
+ },
+ VersionConstraint: "< 4.7.7 (unknown)",
+ VulnerabilityID: "CVE-2021-23369",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "package without CPEs returns error",
+ p: pkg.Package{
+ Name: "some-package",
+ },
+ expected: nil,
+ wantErr: func(t require.TestingT, err error, i ...interface{}) {
+ if !errors.Is(err, ErrEmptyCPEMatch) {
+ t.Errorf("expected %v but got %v", ErrEmptyCPEMatch, err)
+ t.FailNow()
+ }
+ },
+ },
+ {
+ name: "Ensure match is kept for target software that matches the syft package language type",
+ p: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.JavaScript,
+ Type: syftPkg.NpmPkg,
+ },
+ expected: []match.Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{ID: "CVE-2021-23369"},
+ },
+ Package: pkg.Package{
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""),
+ },
+ Name: "handlebars",
+ Version: "0.1",
+ Language: syftPkg.JavaScript,
+ Type: syftPkg.NpmPkg,
+ },
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"},
+ Namespace: "nvd:cpe",
+ Package: match.CPEPackageParameter{
+ Name: "handlebars",
+ Version: "0.1",
+ },
+ },
+ Found: match.CPEResult{
+ CPEs: []string{
+ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
+ },
+ VersionConstraint: "< 4.7.7 (unknown)",
+ VulnerabilityID: "CVE-2021-23369",
+ },
+ Matcher: matcher,
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ actual, err := MatchPackageByCPEs(newCPETestStore(), test.p, matcher)
+ if test.wantErr == nil {
+ test.wantErr = require.NoError
+ }
+ test.wantErr(t, err)
+ assertMatchesUsingIDsForVulnerabilities(t, test.expected, actual)
+ for idx, e := range test.expected {
+ if idx < len(actual) {
+ if d := cmp.Diff(e.Details, actual[idx].Details); d != "" {
+ t.Errorf("unexpected match details (-want +got):\n%s", d)
+ }
+ } else {
+ t.Errorf("expected match details (-want +got)\n%+v:\n", e.Details)
+ }
+ }
+ })
+ }
+}
+
+func TestFilterCPEsByVersion(t *testing.T) {
+ tests := []struct {
+ name string
+ version string
+ vulnerabilityCPEs []string
+ expected []string
+ }{
+ {
+ name: "filter out by simple version",
+ version: "1.0",
+ 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:*:*:*:*:*:*:*",
+ },
+ },
+ {
+ 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 {
+ t.Run(test.name, func(t *testing.T) {
+ // format strings to CPE objects...
+ vulnerabilityCPEs := make([]cpe.CPE, len(test.vulnerabilityCPEs))
+ for idx, c := range test.vulnerabilityCPEs {
+ vulnerabilityCPEs[idx] = cpe.Must(c, "")
+ }
+
+ 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)
+
+ // format CPE objects to string...
+ actualStrs := make([]string, len(actual))
+ for idx, a := range actual {
+ actualStrs[idx] = a.Attributes.BindToFmtString()
+ }
+
+ assert.ElementsMatch(t, test.expected, actualStrs)
+ })
+ }
+}
+
+func TestAddMatchDetails(t *testing.T) {
+ tests := []struct {
+ name string
+ existing []match.Detail
+ new match.Detail
+ expected []match.Detail
+ }{
+ {
+ name: "append new entry -- found not equal",
+ existing: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ },
+ new: match.Detail{
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "totally-different-search",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "totally-different-match",
+ },
+ },
+ },
+ expected: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "totally-different-search",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "totally-different-match",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "append new entry -- searchedBy merge fails",
+ existing: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ },
+ new: match.Detail{
+ SearchedBy: match.CPEParameters{
+ Namespace: "totally-different",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ expected: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "totally-different",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "merge with exiting entry",
+ existing: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ },
+ new: match.Detail{
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "totally-different-search",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ expected: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ "totally-different-search",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "no addition - bad new searchedBy type",
+ existing: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ },
+ new: match.Detail{
+ SearchedBy: "something else!",
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ expected: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "no addition - bad new found type",
+ existing: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ },
+ new: match.Detail{
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: "something-else!",
+ },
+ expected: []match.Detail{
+ {
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
+ },
+ },
+ Found: match.CPEResult{
+ VersionConstraint: "< 2.0 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.expected, addMatchDetails(test.existing, test.new))
+ })
+ }
+}
+
+func TestCPESearchHit_Equals(t *testing.T) {
+ tests := []struct {
+ name string
+ current match.CPEResult
+ other match.CPEResult
+ expected bool
+ }{
+ {
+ name: "different version constraint",
+ current: match.CPEResult{
+ VersionConstraint: "current-constraint",
+ CPEs: []string{
+ "a-cpe",
+ },
+ },
+ other: match.CPEResult{
+ VersionConstraint: "different-constraint",
+ CPEs: []string{
+ "a-cpe",
+ },
+ },
+ expected: false,
+ },
+ {
+ name: "different number of CPEs",
+ current: match.CPEResult{
+ VersionConstraint: "current-constraint",
+ CPEs: []string{
+ "a-cpe",
+ },
+ },
+ other: match.CPEResult{
+ VersionConstraint: "current-constraint",
+ CPEs: []string{
+ "a-cpe",
+ "b-cpe",
+ },
+ },
+ expected: false,
+ },
+ {
+ name: "different CPE value",
+ current: match.CPEResult{
+ VersionConstraint: "current-constraint",
+ CPEs: []string{
+ "a-cpe",
+ },
+ },
+ other: match.CPEResult{
+ VersionConstraint: "current-constraint",
+ CPEs: []string{
+ "b-cpe",
+ },
+ },
+ expected: false,
+ },
+ {
+ name: "matches",
+ current: match.CPEResult{
+ VersionConstraint: "current-constraint",
+ CPEs: []string{
+ "a-cpe",
+ },
+ },
+ other: match.CPEResult{
+ VersionConstraint: "current-constraint",
+ CPEs: []string{
+ "a-cpe",
+ },
+ },
+ expected: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.expected, test.current.Equals(test.other))
+ })
+ }
+}
diff --git a/grype/matcher/internal/distro.go b/grype/matcher/internal/distro.go
new file mode 100644
index 00000000000..65179bc66ba
--- /dev/null
+++ b/grype/matcher/internal/distro.go
@@ -0,0 +1,87 @@
+package internal
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+)
+
+func MatchPackageByDistro(provider vulnerability.Provider, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, []match.IgnoredMatch, error) {
+ if p.Distro == nil {
+ return nil, nil, nil
+ }
+
+ if isUnknownVersion(p.Version) {
+ log.WithFields("package", p.Name).Trace("skipping package with unknown 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)
+ }
+ }
+
+ var matches []match.Match
+ vulns, err := provider.FindVulnerabilities(
+ search.ByPackageName(p.Name),
+ search.ByDistro(*p.Distro),
+ onlyQualifiedPackages(p),
+ onlyVulnerableVersions(verObj),
+ )
+ if err != nil {
+ return nil, nil, fmt.Errorf("matcher failed to fetch distro=%q pkg=%q: %w", p.Distro, p.Name, err)
+ }
+
+ for _, vuln := range vulns {
+ matches = append(matches, match.Match{
+ Vulnerability: vuln,
+ Package: p,
+ Details: []match.Detail{
+ {
+ Type: match.ExactDirectMatch,
+ Matcher: upstreamMatcher,
+ SearchedBy: map[string]interface{}{
+ "distro": map[string]string{
+ "type": p.Distro.Type.String(),
+ "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
+ // was used in the search.
+ "package": map[string]string{
+ "name": p.Name,
+ "version": p.Version,
+ },
+ "namespace": vuln.Namespace,
+ },
+ Found: map[string]interface{}{
+ "vulnerabilityID": vuln.ID,
+ "versionConstraint": vuln.Constraint.String(),
+ },
+ Confidence: 1.0, // TODO: this is hard coded for now
+ },
+ },
+ })
+ }
+ return matches, nil, err
+}
+
+func isUnknownVersion(v string) bool {
+ return strings.ToLower(v) == "unknown"
+}
diff --git a/grype/matcher/internal/distro_test.go b/grype/matcher/internal/distro_test.go
new file mode 100644
index 00000000000..cca7f73f006
--- /dev/null
+++ b/grype/matcher/internal/distro_test.go
@@ -0,0 +1,166 @@
+package internal
+
+import (
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/distro"
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func newMockProviderByDistro() vulnerability.Provider {
+ return mock.VulnerabilityProvider([]vulnerability.Vulnerability{
+ {
+ // direct...
+ PackageName: "neutron",
+ Constraint: version.MustGetConstraint("< 2014.1.5-6", version.DebFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-1",
+ Namespace: "secdb:distro:debian:8",
+ },
+ },
+ {
+ PackageName: "sles_test_package",
+ Constraint: version.MustGetConstraint("< 2014.1.5-6", version.RpmFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-4",
+ Namespace: "secdb:distro:sles:12.5",
+ },
+ },
+ }...)
+}
+
+func TestFindMatchesByPackageDistro(t *testing.T) {
+ p := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "neutron",
+ Version: "2014.1.3-6",
+ Type: syftPkg.DebPkg,
+ Upstreams: []pkg.UpstreamPackage{
+ {
+ Name: "neutron-devel",
+ },
+ },
+ }
+
+ d, err := distro.New(distro.Debian, "8", "")
+ if err != nil {
+ t.Fatal("could not create distro: ", err)
+ }
+ p.Distro = d
+
+ expected := []match.Match{
+ {
+
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-1",
+ },
+ },
+ Package: p,
+ Details: []match.Detail{
+ {
+ Type: match.ExactDirectMatch,
+ Confidence: 1,
+ SearchedBy: map[string]interface{}{
+ "distro": map[string]string{
+ "type": "debian",
+ "version": "8",
+ },
+ "package": map[string]string{
+ "name": "neutron",
+ "version": "2014.1.3-6",
+ },
+ "namespace": "secdb:distro:debian:8",
+ },
+ Found: map[string]interface{}{
+ "versionConstraint": "< 2014.1.5-6 (deb)",
+ "vulnerabilityID": "CVE-2014-fake-1",
+ },
+ Matcher: match.PythonMatcher,
+ },
+ },
+ },
+ }
+
+ store := newMockProviderByDistro()
+ actual, ignored, err := MatchPackageByDistro(store, p, match.PythonMatcher)
+ require.NoError(t, err)
+ require.Empty(t, ignored)
+ assertMatchesUsingIDsForVulnerabilities(t, expected, actual)
+
+ // prove we do not search for unknown versions
+ p.Version = "unknown"
+ actual, ignored, err = MatchPackageByDistro(store, p, match.PythonMatcher)
+ require.NoError(t, err)
+ require.Empty(t, ignored)
+ assert.Empty(t, actual)
+}
+
+func TestFindMatchesByPackageDistroSles(t *testing.T) {
+ p := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "sles_test_package",
+ Version: "2014.1.3-6",
+ Type: syftPkg.RpmPkg,
+ Upstreams: []pkg.UpstreamPackage{
+ {
+ Name: "sles_test_package",
+ },
+ },
+ }
+
+ d, err := distro.New(distro.SLES, "12.5", "")
+ if err != nil {
+ t.Fatal("could not create distro: ", err)
+ }
+ p.Distro = d
+
+ expected := []match.Match{
+ {
+
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-4",
+ },
+ },
+ Package: p,
+ Details: []match.Detail{
+ {
+ Type: match.ExactDirectMatch,
+ Confidence: 1,
+ SearchedBy: map[string]interface{}{
+ "distro": map[string]string{
+ "type": "sles",
+ "version": "12.5",
+ },
+ "package": map[string]string{
+ "name": "sles_test_package",
+ "version": "2014.1.3-6",
+ },
+ "namespace": "secdb:distro:sles:12.5",
+ },
+ Found: map[string]interface{}{
+ "versionConstraint": "< 2014.1.5-6 (rpm)",
+ "vulnerabilityID": "CVE-2014-fake-4",
+ },
+ Matcher: match.PythonMatcher,
+ },
+ },
+ },
+ }
+
+ store := newMockProviderByDistro()
+ actual, ignored, err := MatchPackageByDistro(store, p, match.PythonMatcher)
+ assert.NoError(t, err)
+ require.Empty(t, ignored)
+ assertMatchesUsingIDsForVulnerabilities(t, expected, actual)
+}
diff --git a/grype/matcher/internal/language.go b/grype/matcher/internal/language.go
new file mode 100644
index 00000000000..645d87d4660
--- /dev/null
+++ b/grype/matcher/internal/language.go
@@ -0,0 +1,84 @@
+package internal
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+)
+
+func MatchPackageByLanguage(store vulnerability.Provider, p pkg.Package, matcherType match.MatcherType) ([]match.Match, []match.IgnoredMatch, error) {
+ var matches []match.Match
+ var ignored []match.IgnoredMatch
+
+ for _, name := range store.PackageSearchNames(p) {
+ nameMatches, nameIgnores, err := MatchPackageByEcosystemPackageName(store, p, name, matcherType)
+ if err != nil {
+ return nil, nil, err
+ }
+ matches = append(matches, nameMatches...)
+ ignored = append(ignored, nameIgnores...)
+ }
+
+ return matches, ignored, nil
+}
+
+func MatchPackageByEcosystemPackageName(provider vulnerability.Provider, p pkg.Package, packageName string, matcherType match.MatcherType) ([]match.Match, []match.IgnoredMatch, error) {
+ if isUnknownVersion(p.Version) {
+ log.WithFields("package", p.Name).Trace("skipping package with unknown version")
+ 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
+ }
+ return nil, nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err)
+ }
+
+ var matches []match.Match
+ vulns, err := provider.FindVulnerabilities(
+ search.ByEcosystem(p.Language, p.Type),
+ search.ByPackageName(packageName),
+ onlyQualifiedPackages(p),
+ onlyVulnerableVersions(verObj),
+ onlyNonWithdrawnVulnerabilities(),
+ )
+ if err != nil {
+ return nil, nil, fmt.Errorf("matcher failed to fetch language=%q pkg=%q: %w", p.Language, p.Name, err)
+ }
+
+ for _, vuln := range vulns {
+ matches = append(matches, match.Match{
+ Vulnerability: vuln,
+ Package: p,
+ Details: []match.Detail{
+ {
+ Type: match.ExactDirectMatch,
+ Confidence: 1.0, // TODO: this is hard coded for now
+ Matcher: matcherType,
+ SearchedBy: map[string]interface{}{
+ "language": string(p.Language),
+ "namespace": vuln.Namespace,
+ "package": map[string]string{
+ "name": p.Name,
+ "version": p.Version,
+ },
+ },
+ Found: map[string]interface{}{
+ "vulnerabilityID": vuln.ID,
+ "versionConstraint": vuln.Constraint.String(),
+ },
+ },
+ },
+ })
+ }
+ return matches, nil, err
+}
diff --git a/grype/matcher/internal/language_test.go b/grype/matcher/internal/language_test.go
new file mode 100644
index 00000000000..307ea73db1d
--- /dev/null
+++ b/grype/matcher/internal/language_test.go
@@ -0,0 +1,137 @@
+package internal
+
+import (
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func newMockProviderByLanguage() vulnerability.Provider {
+ return mock.VulnerabilityProvider([]vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-1",
+ Namespace: "github:language:ruby",
+ },
+ PackageName: "activerecord",
+ // make sure we find it with semVer constraint
+ Constraint: version.MustGetConstraint("< 3.7.6", version.SemanticFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-2",
+ Namespace: "github:language:ruby",
+ },
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.4", version.GemFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-1",
+ Namespace: "github:language:ruby",
+ },
+ PackageName: "nokogiri",
+ // make sure we find it with gem version constraint
+ Constraint: version.MustGetConstraint("< 1.7.6", version.GemFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-2",
+ Namespace: "github:language:ruby",
+ },
+ PackageName: "nokogiri",
+ Constraint: version.MustGetConstraint("< 1.7.4", version.SemanticFormat),
+ },
+ }...)
+}
+
+func expectedMatch(p pkg.Package, constraint string) []match.Match {
+ return []match.Match{
+ {
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2017-fake-1",
+ },
+ },
+ Package: p,
+ Details: []match.Detail{
+ {
+ Type: match.ExactDirectMatch,
+ Confidence: 1,
+ SearchedBy: map[string]interface{}{
+ "language": "ruby",
+ "namespace": "github:language:ruby",
+ "package": map[string]string{"name": p.Name, "version": p.Version},
+ },
+ Found: map[string]interface{}{
+ "versionConstraint": constraint,
+ "vulnerabilityID": "CVE-2017-fake-1",
+ },
+ Matcher: match.RubyGemMatcher,
+ },
+ },
+ },
+ }
+}
+
+func TestFindMatchesByPackageLanguage(t *testing.T) {
+ cases := []struct {
+ p pkg.Package
+ constraint string
+ assertEmpty bool
+ }{
+ {
+ constraint: "< 3.7.6 (semver)",
+ p: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "activerecord",
+ Version: "3.7.5",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ },
+ },
+ {
+ constraint: "< 1.7.6 (semver)",
+ p: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "nokogiri",
+ Version: "1.7.5",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ },
+ },
+ {
+ p: pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "nokogiri",
+ Version: "unknown",
+ Language: syftPkg.Ruby,
+ Type: syftPkg.GemPkg,
+ },
+ assertEmpty: true,
+ },
+ }
+
+ store := newMockProviderByLanguage()
+ for _, c := range cases {
+ t.Run(c.p.Name, func(t *testing.T) {
+ actual, ignored, err := MatchPackageByLanguage(store, c.p, match.RubyGemMatcher)
+ require.NoError(t, err)
+ require.Empty(t, ignored)
+ if c.assertEmpty {
+ assert.Empty(t, actual)
+ return
+ }
+ assertMatchesUsingIDsForVulnerabilities(t, expectedMatch(c.p, c.constraint), actual)
+ })
+ }
+}
diff --git a/grype/matcher/internal/only_non_withdrawn_vulnerabilities.go b/grype/matcher/internal/only_non_withdrawn_vulnerabilities.go
new file mode 100644
index 00000000000..e0efa8aa728
--- /dev/null
+++ b/grype/matcher/internal/only_non_withdrawn_vulnerabilities.go
@@ -0,0 +1,18 @@
+package internal
+
+import (
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+// onlyNonWithdrawnVulnerabilities returns a criteria object that tests affected vulnerability is not withdrawn/rejected
+func onlyNonWithdrawnVulnerabilities() vulnerability.Criteria {
+ return search.ByFunc(func(v vulnerability.Vulnerability) (bool, string, error) {
+ // we should be using enumerations from all supported schema versions, but constants should not be imported here
+ isWithdrawn := v.Status == "withdrawn" || v.Status == "rejected"
+ if isWithdrawn {
+ return false, "vulnerability is withdrawn or rejected", nil
+ }
+ return true, "", nil
+ })
+}
diff --git a/grype/matcher/internal/only_qualified_packages.go b/grype/matcher/internal/only_qualified_packages.go
new file mode 100644
index 00000000000..db4e2ffec90
--- /dev/null
+++ b/grype/matcher/internal/only_qualified_packages.go
@@ -0,0 +1,26 @@
+package internal
+
+import (
+ "fmt"
+
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+// onlyQualifiedPackages returns a criteria object that tests vulnerability qualifiers against the provided package
+func onlyQualifiedPackages(p pkg.Package) vulnerability.Criteria {
+ return search.ByFunc(func(vuln vulnerability.Vulnerability) (bool, string, error) {
+ for _, qualifier := range vuln.PackageQualifiers {
+ satisfied, err := qualifier.Satisfied(p)
+ if err != nil {
+ return satisfied, fmt.Sprintf("unable to evaluate qualifier: %s", err.Error()), err
+ }
+ if !satisfied {
+ // TODO: qualifiers don't have a good string representation
+ return false, fmt.Sprintf("package does not satisfy qualifier: %#v", qualifier), nil
+ }
+ }
+ return true, "", nil // all qualifiers passed
+ })
+}
diff --git a/grype/matcher/internal/only_vulnerable_targets.go b/grype/matcher/internal/only_vulnerable_targets.go
new file mode 100644
index 00000000000..e1e9a65d38a
--- /dev/null
+++ b/grype/matcher/internal/only_vulnerable_targets.go
@@ -0,0 +1,193 @@
+package internal
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/facebookincubator/nvdtools/wfn"
+ "github.com/scylladb/go-set/strset"
+
+ "github.com/anchore/grype/grype/internal"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/syft/syft/cpe"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+// onlyVulnerableTargets returns a criteria object that tests vulnerability qualifiers against the package vulnerability rules.
+// TODO: in the future this should be moved to underneath the store to avoid the need to recompute CPE comparisons and to leverage ecosystem aliases for target software
+func onlyVulnerableTargets(p pkg.Package) vulnerability.Criteria {
+ return search.ByFunc(func(v vulnerability.Vulnerability) (bool, string, error) {
+ matches, reasons := isVulnerableTarget(p, v)
+ return matches, reasons, nil
+ })
+}
+
+// Determines if a vulnerability is an accurate match using the vulnerability's cpes' target software
+func isVulnerableTarget(p pkg.Package, vuln vulnerability.Vulnerability) (bool, string) {
+ // Exclude OS package types from this logic, since they could be embedding any type of ecosystem package
+ if isOSPackage(p) {
+ return true, ""
+ }
+
+ packageTargetSwSet, vulnTargetSwSet := matchTargetSoftware(p.CPEs, vuln.CPEs)
+ if len(vuln.CPEs) > 0 && packageTargetSwSet.IsEmpty() {
+ reason := fmt.Sprintf("vulnerability target software(s) (%q) do not align with %s", strings.Join(vulnTargetSwSet.List(), ", "), packageElements(p, packageTargetSwSet.List()))
+ return false, reason
+ }
+
+ // only strictly use CPE attributes to filter binary and unknown package types
+ if p.Type == syftPkg.BinaryPkg || p.Type == syftPkg.UnknownPkg || p.Type == "" {
+ if hasIntersectingTargetSoftware(packageTargetSwSet, vulnTargetSwSet) {
+ // we have at least one target software in common
+ return true, ""
+ }
+
+ // the package has a * target software, so should match with anything that's on the CPE.
+ // note that this is two way (either the package has a * or the vuln has a * target software).
+ if packageTargetSwSet.Has(wfn.Any) || vulnTargetSwSet.Has(wfn.Any) {
+ return true, ""
+ }
+
+ reason := fmt.Sprintf("vulnerability target software(s) (%q) do not align with %s", strings.Join(vulnTargetSwSet.List(), ", "), packageElements(p, packageTargetSwSet.List()))
+ return false, reason
+ }
+
+ // There are quite a few cases within java where other ecosystem components (particularly javascript packages)
+ // are embedded directly within jar files, so we can't yet make this assumption with java as it will cause dropping
+ // of valid vulnerabilities that syft has specific logic https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go#L48-L75
+ // to ensure will be surfaced
+ if p.Language == syftPkg.Java {
+ return true, ""
+ }
+
+ // if there are no CPEs then we can't make a decision
+ if len(vuln.CPEs) == 0 {
+ return true, ""
+ }
+
+ if hasIntersectingTargetSoftware(packageTargetSwSet, vulnTargetSwSet) {
+ // we have at least one target software in common
+ return true, ""
+ }
+
+ return refuteTargetSoftwareByPackageAttributes(p, vuln, packageTargetSwSet)
+}
+
+func refuteTargetSoftwareByPackageAttributes(p pkg.Package, vuln vulnerability.Vulnerability, packageTargetSwSet *strset.Set) (bool, string) {
+ // this is purely based on package attributes and does not consider any package CPE target softwares (which the store already considers)
+ var mismatchedTargetSoftware []string
+ for _, c := range vuln.CPEs {
+ targetSW := c.Attributes.TargetSW
+ mismatchWithUnknownLanguage := syftPkg.LanguageByName(targetSW) != p.Language && isUnknownTarget(targetSW)
+ unspecifiedTargetSW := targetSW == wfn.Any || targetSW == wfn.NA
+ matchesByLanguage := syftPkg.LanguageByName(targetSW) == p.Language
+ matchesByPackageType := internal.CPETargetSoftwareToPackageType(targetSW) == p.Type
+ if unspecifiedTargetSW || matchesByLanguage || matchesByPackageType || mismatchWithUnknownLanguage {
+ return true, ""
+ }
+ mismatchedTargetSoftware = append(mismatchedTargetSoftware, targetSW)
+ }
+
+ reason := fmt.Sprintf("vulnerability target software(s) (%q) do not align with %s", strings.Join(mismatchedTargetSoftware, ", "), packageElements(p, packageTargetSwSet.List()))
+ return false, reason
+}
+
+func isOSPackage(p pkg.Package) bool {
+ return p.Type == syftPkg.AlpmPkg || p.Type == syftPkg.ApkPkg || p.Type == syftPkg.DebPkg || p.Type == syftPkg.KbPkg || p.Type == syftPkg.PortagePkg || p.Type == syftPkg.RpmPkg
+}
+
+func isUnknownTarget(targetSW string) bool {
+ if syftPkg.LanguageByName(targetSW) != syftPkg.UnknownLanguage {
+ return false
+ }
+
+ // There are some common target software CPE components which are not currently
+ // supported by syft but are signifcant sources of false positives and should be
+ // considered known for the purposes of filtering here
+ known := map[string]bool{
+ "joomla": true,
+ "joomla\\!": true,
+ "drupal": true,
+ }
+
+ if _, ok := known[targetSW]; ok {
+ return false
+ }
+
+ return true
+}
+
+func matchTargetSoftware(pkgCPEs []cpe.CPE, vulnCPEs []cpe.CPE) (*strset.Set, *strset.Set) {
+ pkgTsw := strset.New()
+ vulnTsw := strset.New()
+ for _, c := range vulnCPEs {
+ for _, p := range pkgCPEs {
+ if matchesAttributesExceptVersionAndTSW(c.Attributes, p.Attributes) {
+ // include any value including empty string (which means ANY value)
+ pkgTsw.Add(p.Attributes.TargetSW)
+ vulnTsw.Add(c.Attributes.TargetSW)
+ }
+ }
+ }
+ return pkgTsw, vulnTsw
+}
+
+func matchesAttributesExceptVersionAndTSW(a1 cpe.Attributes, a2 cpe.Attributes) bool {
+ // skip version, update, and target software
+ if !matchesAttribute(a1.Product, a2.Product) ||
+ !matchesAttribute(a1.Vendor, a2.Vendor) ||
+ !matchesAttribute(a1.Part, a2.Part) ||
+ !matchesAttribute(a1.Language, a2.Language) ||
+ !matchesAttribute(a1.SWEdition, a2.SWEdition) ||
+ !matchesAttribute(a1.TargetHW, a2.TargetHW) ||
+ !matchesAttribute(a1.Other, a2.Other) ||
+ !matchesAttribute(a1.Edition, a2.Edition) {
+ return false
+ }
+ return true
+}
+
+func matchesAttribute(a1, a2 string) bool {
+ return a1 == "" || a2 == "" || strings.EqualFold(a1, a2)
+}
+
+func hasIntersectingTargetSoftware(set1, set2 *strset.Set) bool {
+ set1Pkg := pkgTypesFromTargetSoftware(set1.List())
+ set2Pkg := pkgTypesFromTargetSoftware(set2.List())
+ intersection := strset.Intersection(set1Pkg, set2Pkg)
+ return !intersection.IsEmpty()
+}
+
+func pkgTypesFromTargetSoftware(ts []string) *strset.Set {
+ pkgTypes := strset.New()
+ for _, ts := range ts {
+ pt := internal.CPETargetSoftwareToPackageType(ts)
+ if pt != "" {
+ pkgTypes.Add(string(pt))
+ }
+ }
+ return pkgTypes
+}
+
+func packageElements(p pkg.Package, ts []string) string {
+ nameVersion := fmt.Sprintf("%s@%s", p.Name, p.Version)
+
+ pType := string(p.Type)
+ if pType == "" {
+ pType = "?"
+ }
+
+ pLanguage := string(p.Language)
+ if pLanguage == "" {
+ pLanguage = "?"
+ }
+
+ targetSW := strings.Join(ts, ",")
+ if (len(ts) == 0) || (len(ts) == 1 && ts[0] == wfn.Any) {
+ targetSW = "*"
+ }
+
+ return fmt.Sprintf("pkg(%s type=%q language=%q targets=%q)", nameVersion, pType, pLanguage, targetSW)
+}
diff --git a/grype/matcher/internal/only_vulnerable_targets_test.go b/grype/matcher/internal/only_vulnerable_targets_test.go
new file mode 100644
index 00000000000..f33efb2b774
--- /dev/null
+++ b/grype/matcher/internal/only_vulnerable_targets_test.go
@@ -0,0 +1,507 @@
+package internal
+
+import (
+ "testing"
+
+ "github.com/scylladb/go-set/strset"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/syft/syft/cpe"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func TestIsVulnerableTarget(t *testing.T) {
+ tests := []struct {
+ name string
+ pkg pkg.Package
+ vuln vulnerability.Vulnerability
+ expectedMatches bool
+ expectedReason string
+ }{
+ {
+ name: "OS package should always match",
+ pkg: pkg.Package{
+ Name: "openssl",
+ Version: "1.1.1k",
+ Type: syftPkg.RpmPkg,
+ Language: syftPkg.UnknownLanguage,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:openssl:openssl:1.1.1k:*:*:*:*:*:*:*", ""),
+ },
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2021-3449",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "openssl",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:openssl:openssl:1.1.1k:*:*:*:*:*:*:*", ""),
+ },
+ },
+ expectedMatches: true,
+ },
+ {
+ name: "binary package should always match",
+ pkg: pkg.Package{
+ Name: "bash",
+ Version: "5.0.17",
+ Type: syftPkg.BinaryPkg,
+ Language: syftPkg.UnknownLanguage,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:gnu:bash:5.0.17:*:*:*:*:*:*:*", ""),
+ },
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-12345",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "bash",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:gnu:bash:5.0.17:*:*:*:*:*:*:*", ""),
+ },
+ },
+ expectedMatches: true,
+ },
+ {
+ name: "unknown package should always match",
+ pkg: pkg.Package{
+ Name: "unknown-pkg",
+ Version: "1.0.0",
+ Type: syftPkg.UnknownPkg,
+ Language: syftPkg.UnknownLanguage,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:unknown:unknown-pkg:1.0.0:*:*:*:*:*:*:*", ""),
+ },
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2021-98765",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "unknown-pkg",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:unknown:unknown-pkg:1.0.0:*:*:*:*:*:*:*", ""),
+ },
+ },
+ expectedMatches: true,
+ },
+ {
+ name: "java package should always match",
+ pkg: pkg.Package{
+ Name: "log4j-core",
+ Version: "2.14.1",
+ Type: syftPkg.JavaPkg,
+ Language: syftPkg.Java,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""),
+ },
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2021-44228",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "log4j-core",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""),
+ },
+ },
+ expectedMatches: true,
+ },
+ {
+ name: "package with no CPEs should fail",
+ pkg: pkg.Package{
+ Name: "example-lib",
+ Version: "1.0.0",
+ Type: syftPkg.NpmPkg,
+ Language: syftPkg.JavaScript,
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2021-87654",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "example-lib",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:example:example-lib:1.0.0:*:*:*:*:*:*:*", ""),
+ },
+ },
+ expectedMatches: false,
+ expectedReason: `vulnerability target software(s) ("") do not align with pkg(example-lib@1.0.0 type="npm" language="javascript" targets="*")`,
+ },
+ {
+ name: "vulnerability with no CPEs should match",
+ pkg: pkg.Package{
+ Name: "example-lib",
+ Version: "1.0.0",
+ Type: syftPkg.NpmPkg,
+ Language: syftPkg.JavaScript,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:example:example-lib:1.0.0:*:*:*:*:*:*:*", ""),
+ },
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2021-87654",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "example-lib",
+ },
+ expectedMatches: true,
+ },
+ {
+ name: "package with wildcard targetSW should match",
+ pkg: pkg.Package{
+ Name: "react",
+ Version: "17.0.2",
+ Type: syftPkg.NpmPkg,
+ Language: syftPkg.JavaScript,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:facebook:react:17.0.2:*:*:*:*:*:*:*", ""),
+ },
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2021-12345",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "react",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:facebook:react:17.0.2:*:*:*:*:node.js:*:*", ""),
+ },
+ },
+ expectedMatches: true,
+ },
+ {
+ name: "intersecting target software should match",
+ pkg: pkg.Package{
+ Name: "lodash",
+ Version: "4.17.20",
+ Type: syftPkg.NpmPkg,
+ Language: syftPkg.JavaScript,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:lodash:lodash:4.17.20:*:*:*:*:node.js:*:*", ""),
+ },
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2021-23337",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "lodash",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:lodash:lodash:4.17.20:*:*:*:*:node.js:*:*", ""),
+ },
+ },
+ expectedMatches: true,
+ },
+ {
+ name: "non-intersecting target software with matching language should match",
+ pkg: pkg.Package{
+ Name: "express",
+ Version: "4.17.1",
+ Type: syftPkg.RpmPkg, // important!
+ Language: syftPkg.JavaScript, // we're using this to match against the vuln TSW
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:expressjs:express:4.17.1:*:*:*:*:react:*:*", ""),
+ },
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2022-24999",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "express",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:expressjs:express:4.17.1:*:*:*:*:node.js:*:*", ""),
+ },
+ },
+ expectedMatches: true,
+ },
+ {
+ name: "non-intersecting target software with matching package type should fail",
+ pkg: pkg.Package{
+ Name: "moment",
+ Version: "2.29.1",
+ Type: syftPkg.NpmPkg, // we're using this to match against the vuln TSW
+ Language: syftPkg.CPP, // important!
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:moment:moment:2.29.1:*:*:*:*:doesntmatter:*:*", ""),
+ },
+ },
+ vuln: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2022-31129",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "moment",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:moment:moment:2.29.1:*:*:*:*:node.js:*:*", ""),
+ },
+ },
+ expectedMatches: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ matches, reason := isVulnerableTarget(test.pkg, test.vuln)
+
+ assert.Equal(t, test.expectedMatches, matches, "matches result should be as expected")
+ assert.Equal(t, test.expectedReason, reason, "reason should match expected")
+ })
+ }
+}
+
+func Test_isUnknownTarget(t *testing.T) {
+ tests := []struct {
+ name string
+ targetSW string
+ expected bool
+ }{
+ {name: "supported syft language", targetSW: "python", expected: false},
+ {name: "supported non-syft language CPE component", targetSW: "joomla", expected: false},
+ {name: "unknown component", targetSW: "abc", expected: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ u := isUnknownTarget(test.targetSW)
+ assert.Equal(t, test.expected, u)
+ })
+ }
+}
+
+func TestPkgTypesFromTargetSoftware(t *testing.T) {
+ tests := []struct {
+ name string
+ input []string
+ expected []syftPkg.Type
+ }{
+ {
+ name: "empty input",
+ input: []string{},
+ expected: []syftPkg.Type{},
+ },
+ {
+ name: "single input with known mapping",
+ input: []string{"node.js"},
+ expected: []syftPkg.Type{syftPkg.NpmPkg},
+ },
+ {
+ name: "multiple inputs with known mappings",
+ input: []string{"python", "ruby", "java"},
+ expected: []syftPkg.Type{syftPkg.PythonPkg, syftPkg.GemPkg, syftPkg.JavaPkg},
+ },
+ {
+ name: "case insensitive input",
+ input: []string{"Python", "RUBY", "Java"},
+ expected: []syftPkg.Type{syftPkg.PythonPkg, syftPkg.GemPkg, syftPkg.JavaPkg},
+ },
+ {
+ name: "mixed known and unknown inputs",
+ input: []string{"python", "unknown", "ruby"},
+ expected: []syftPkg.Type{syftPkg.PythonPkg, syftPkg.GemPkg},
+ },
+ {
+ name: "all unknown inputs",
+ input: []string{"unknown1", "unknown2", "unknown3"},
+ expected: []syftPkg.Type{},
+ },
+ {
+ name: "inputs with spaces and hyphens",
+ input: []string{"redhat-enterprise-linux", "jenkins ci"},
+ expected: []syftPkg.Type{syftPkg.RpmPkg, syftPkg.JavaPkg},
+ },
+ {
+ name: "aliases for the same package type",
+ input: []string{"nodejs", "npm", "javascript"},
+ expected: []syftPkg.Type{syftPkg.NpmPkg},
+ },
+ {
+ name: "wildcards and special characters should be ignored",
+ input: []string{"*", "?", ""},
+ expected: []syftPkg.Type{},
+ },
+ {
+ name: "Linux distributions",
+ input: []string{"alpine", "debian", "redhat", "gentoo"},
+ expected: []syftPkg.Type{syftPkg.ApkPkg, syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.PortagePkg},
+ },
+ {
+ name: ".NET ecosystem",
+ input: []string{".net", "asp.net", "c#"},
+ expected: []syftPkg.Type{syftPkg.DotnetPkg},
+ },
+ {
+ name: "JavaScript ecosystem",
+ input: []string{"javascript", "node.js", "jquery"},
+ expected: []syftPkg.Type{syftPkg.NpmPkg},
+ },
+ {
+ name: "Java ecosystem",
+ input: []string{"java", "maven", "kafka", "log4j"},
+ expected: []syftPkg.Type{syftPkg.JavaPkg},
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ actual := pkgTypesFromTargetSoftware(test.input)
+
+ var actualTypes []syftPkg.Type
+ for _, typeStr := range actual.List() {
+ actualTypes = append(actualTypes, syftPkg.Type(typeStr))
+ }
+
+ assert.ElementsMatch(t, test.expected, actualTypes, "package types should match")
+ })
+ }
+}
+
+func TestHasIntersectingTargetSoftware(t *testing.T) {
+ tests := []struct {
+ name string
+ set1 []string
+ set2 []string
+ expected bool
+ }{
+ // basic assertions around sets normalized to package types
+ {
+ name: "empty sets",
+ set1: []string{},
+ set2: []string{},
+ expected: false,
+ },
+ {
+ name: "first set empty",
+ set1: []string{},
+ set2: []string{"nodejs", "python"},
+ expected: false,
+ },
+ {
+ name: "second set empty",
+ set1: []string{"java", "ruby"},
+ set2: []string{},
+ expected: false,
+ },
+ {
+ name: "intersecting sets - direct match",
+ set1: []string{"nodejs", "python"},
+ set2: []string{"nodejs", "ruby"},
+ expected: true,
+ },
+ {
+ name: "intersecting sets - aliases",
+ set1: []string{"node.js"},
+ set2: []string{"npm"},
+ expected: true,
+ },
+ {
+ name: "non-intersecting sets",
+ set1: []string{"python", "ruby"},
+ set2: []string{"java", "golang"},
+ expected: false,
+ },
+ {
+ name: "multiple intersections",
+ set1: []string{"python", "ruby", "nodejs"},
+ set2: []string{"javascript", "python", "java"},
+ expected: true,
+ },
+ {
+ name: "case insensitive",
+ set1: []string{"Python", "Ruby"},
+ set2: []string{"python", "java"},
+ expected: true,
+ },
+ {
+ name: "wildcard in first set",
+ set1: []string{"*"},
+ set2: []string{"nodejs", "python"},
+ expected: false, // * doesn't map to a package type
+ },
+ {
+ name: "special linux distro aliases",
+ set1: []string{"rhel", "opensuse"},
+ set2: []string{"redhat"},
+ expected: true,
+ },
+ {
+ name: "different terminology for same ecosystem",
+ set1: []string{"c#"},
+ set2: []string{"dotnet"},
+ expected: true,
+ },
+ {
+ name: "spaces and hyphens handling",
+ set1: []string{"jenkins ci"},
+ set2: []string{"jenkins-ci"},
+ expected: true,
+ },
+
+ // ecosystem specific cases
+ {
+ name: "npm package vs node.js vulnerability",
+ set1: []string{"npm"},
+ set2: []string{"node.js"},
+ expected: true,
+ },
+ {
+ name: "python package vs django vulnerability",
+ set1: []string{"python"},
+ set2: []string{"django"},
+ expected: false, // django is not mapped to a package type in the current implementation
+ },
+ {
+ name: "java package vs multiple java ecosystem vulnerabilities",
+ set1: []string{"java"},
+ set2: []string{"tomcat", "log4j", "maven"},
+ expected: true,
+ },
+ {
+ name: "linux distributions match with different aliases",
+ set1: []string{"redhat"},
+ set2: []string{"centos", "fedora", "rhel"},
+ expected: true,
+ },
+ {
+ name: "no common package types",
+ set1: []string{"python", "ruby"},
+ set2: []string{"nodejs", "php"},
+ expected: false,
+ },
+ {
+ name: "mixed case and formatting",
+ set1: []string{"Node.js", "Ruby-On-Rails"},
+ set2: []string{"javascript", "gem"},
+ expected: true,
+ },
+ {
+ name: ".NET ecosystem different terms",
+ set1: []string{".net-framework"},
+ set2: []string{"c#", "nuget"},
+ expected: true,
+ },
+ {
+ name: "WordPress ecosystem",
+ set1: []string{"wordpress"},
+ set2: []string{"wordpress_plugin"},
+ expected: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ set1 := strset.New(test.set1...)
+ set2 := strset.New(test.set2...)
+
+ actual := hasIntersectingTargetSoftware(set1, set2)
+ assert.Equal(t, test.expected, actual, "integrated target software intersection should match expected")
+ })
+ }
+}
diff --git a/grype/matcher/internal/only_vulnerable_versions.go b/grype/matcher/internal/only_vulnerable_versions.go
new file mode 100644
index 00000000000..bd480411aaa
--- /dev/null
+++ b/grype/matcher/internal/only_vulnerable_versions.go
@@ -0,0 +1,18 @@
+package internal
+
+import (
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+// onlyVulnerableVersion returns a criteria object that tests affected vulnerability ranges against the provided version
+func onlyVulnerableVersions(v *version.Version) vulnerability.Criteria {
+ if v == nil || v.Raw == "" {
+ // if no version is provided, match everything
+ return search.ByFunc(func(_ vulnerability.Vulnerability) (bool, string, error) {
+ return true, "", nil
+ }) // since we return true the summary is not used
+ }
+ return search.ByVersion(*v)
+}
diff --git a/grype/search/utils_test.go b/grype/matcher/internal/utils_test.go
similarity index 67%
rename from grype/search/utils_test.go
rename to grype/matcher/internal/utils_test.go
index d2104f11852..5e44acfa3ce 100644
--- a/grype/search/utils_test.go
+++ b/grype/matcher/internal/utils_test.go
@@ -1,10 +1,10 @@
-package search
+package internal
import (
"testing"
"github.com/go-test/deep"
- "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/vulnerability"
@@ -12,10 +12,10 @@ import (
func assertMatchesUsingIDsForVulnerabilities(t testing.TB, expected, actual []match.Match) {
t.Helper()
- assert.Len(t, actual, len(expected))
+ require.Len(t, actual, len(expected))
for idx, a := range actual {
// only compare the vulnerability ID, nothing else
- a.Vulnerability = vulnerability.Vulnerability{ID: a.Vulnerability.ID}
+ a.Vulnerability = vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: a.Vulnerability.ID}}
for _, d := range deep.Equal(expected[idx], a) {
t.Errorf("diff idx=%d: %+v", idx, d)
}
diff --git a/grype/matcher/java/matcher.go b/grype/matcher/java/matcher.go
index 340c269750a..218200aca8f 100644
--- a/grype/matcher/java/matcher.go
+++ b/grype/matcher/java/matcher.go
@@ -1,13 +1,15 @@
package java
import (
+ "context"
"fmt"
"net/http"
+ "strings"
+ "time"
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/log"
syftPkg "github.com/anchore/syft/syft/pkg"
@@ -25,6 +27,7 @@ type Matcher struct {
type ExternalSearchConfig struct {
SearchMavenUpstream bool
MavenBaseURL string
+ MavenRateLimit time.Duration
}
type MatcherConfig struct {
@@ -34,11 +37,8 @@ type MatcherConfig struct {
func NewJavaMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
- cfg: cfg,
- MavenSearcher: &mavenSearch{
- client: http.DefaultClient,
- baseURL: cfg.MavenBaseURL,
- },
+ cfg: cfg,
+ MavenSearcher: newMavenSearch(http.DefaultClient, cfg.MavenBaseURL, cfg.MavenRateLimit),
}
}
@@ -50,49 +50,84 @@ func (m *Matcher) Type() match.MatcherType {
return match.JavaMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
var matches []match.Match
+
if m.cfg.SearchMavenUpstream {
- upstreamMatches, err := m.matchUpstreamMavenPackages(store, d, p)
+ upstreamMatches, err := m.matchUpstreamMavenPackages(store, p)
if err != nil {
- log.Debugf("failed to match against upstream data for %s: %v", p.Name, err)
+ if strings.Contains(err.Error(), "no artifact found") {
+ log.Debugf("no upstream maven artifact found for %s", p.Name)
+ } else {
+ 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...)
}
}
- criteria := search.CommonCriteria
- if m.cfg.UseCPEs {
- criteria = append(criteria, search.ByCPE)
- }
- criteriaMatches, err := search.ByCriteria(store, d, p, m.Type(), criteria...)
+
+ criteriaMatches, ignores, err := internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)
if err != nil {
- return nil, fmt.Errorf("failed to match by exact package: %w", err)
+ return nil, nil, fmt.Errorf("failed to match by exact package: %w", err)
}
matches = append(matches, criteriaMatches...)
- return matches, nil
+
+ return matches, ignores, nil
}
-func (m *Matcher) matchUpstreamMavenPackages(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) matchUpstreamMavenPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) {
var matches []match.Match
- if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok {
- for _, digest := range metadata.ArchiveDigests {
- if digest.Algorithm == "sha1" {
- indirectPackage, err := m.GetMavenPackageBySha(digest.Value)
- if err != nil {
- return nil, err
- }
- indirectMatches, err := search.ByPackageLanguage(store, d, *indirectPackage, m.Type())
- if err != nil {
- return nil, err
- }
- matches = append(matches, indirectMatches...)
+ ctx := context.Background()
+
+ // 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_integration_test.go b/grype/matcher/java/matcher_integration_test.go
new file mode 100644
index 00000000000..444faae16c3
--- /dev/null
+++ b/grype/matcher/java/matcher_integration_test.go
@@ -0,0 +1,689 @@
+//go:build api_limits
+// +build api_limits
+
+package java
+
+import (
+ "context"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+)
+
+// TestMavenSearch_GetMavenPackageBySha tests the GetMavenPackageBySha method of the MavenSearch struct.
+// This is an integration test and requires network access to search.maven.org.
+// It is not intended to be run as part of the normal test suite.
+// Use this to validate rate limiting in [maven_search.go] and the ability to fetch package data from maven.org.
+func TestMavenSearch_GetMavenPackageBySha(t *testing.T) {
+ ctx := context.Background()
+
+ ms := newMavenSearch(http.DefaultClient, "https://search.maven.org/solrsearch/select")
+
+ // Known SHA1s to test with, using a large number of known good SHA1s to validate rate limiting
+ // This is not typical but for Images with a large number of Java packages, this is a good test
+ // to ensure that the rate limiting is working as expected and we don't silently fail and loose scan results
+ shas := []string{
+ "bb7b7ec0379982b97c62cd17465cb6d9155f68e8",
+ "b45b49c1ec5c5fc48580412d0ca635e1833110ea",
+ "245ceca7bdf3190fbb977045c852d5f3c8efece1",
+ "485de3a253e23f645037828c07f1d7f1af40763a",
+ "97662c999c6b2fbf2ee50e814a34639c1c1d22de",
+ "21608dd8b3853da69c4862fbaf9b35b326dc0ddc",
+ "a9cd24fe92272ad1f084d98cd7edeffcd9de720f",
+ "eab9a4baae8de96a24c04219236363d0ca73e8a9",
+ "3647d00620a91360990c9680f29fbcc22d69c2ee",
+ "b957089deb654647da320ad7507b0a4b5ce23813",
+ "bd0cd7ad1e3791a8a0929df0dcdbffc02fd0bab4",
+ "0d1efd839d539481952a9757834054239774f057",
+ "f6148c941e4ec2f314b285e6e4e995f61374aa2f",
+ "502008366a98296ce95c62397b1cb7e06521a195",
+ "92b2a5b7fb0c6a8dcd839d98af2e186f1e98b8ca",
+ "64e6d9608f30eefbe807e65c148018065f971ca6",
+ "095454c18fb12f8fcdbeae4747adfa29bfe6bf17",
+ "0322a158f88b2a18b429133d91459dfa38bf9f55",
+ "f18ebbe9a3145b9ce99733f5a0b7d505be9ae71e",
+ "526df0db4c22be3eb490dab2b4ef979032e3588d",
+ "521694be357010738e7bc612089df8fcc970a0d5",
+ "50d87efaed036c7df71f766ca13aa8783a774ce9",
+ "e8b2cbfe10d9cdcdc29961943b1c6c40f42e2f32",
+ "3c0daebd5f0e1ce72cc50c818321ac957aeb5d70",
+ "919f0dfe192fb4e063e7dacadee7f8bb9a2672a9",
+ "8ceead41f4e71821919dbdb7a9847608f1a938cb",
+ "a1678ba907bf92691d879fef34e1a187038f9259",
+ "83cd2cd674a217ade95a4bb83a8a14f351f48bd0",
+ "6b0acabea7bb3da058200a77178057e47e25cb69",
+ "31c746001016c6226bd7356c9f87a6a084ce3715",
+ "cd9cd41361c155f3af0f653009dcecb08d8b4afd",
+ "2609e36f18f7e8d593cc1cddfb2ac776dc96b8e0",
+ "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8",
+ "ca773f9985c9f4104d76028629026c69c641923c",
+ "a231e0d844d2721b0fa1b238006d15c6ded6842a",
+ "8e6300ef51c1d801a7ed62d07cd221aca3a90640",
+ "379e0250f7a4a42c66c5e94e14d4c4491b3c2ed3",
+ "4b071f211b37c38e0e9f5998550197c8593f6ad8",
+ "1f2a432d1212f5c352ae607d7b61dcae20c20af5",
+ "a3662cf1c1d592893ffe08727f78db35392fa302",
+ "78d2ecd61318b5a58cd04fb237636c0e86b77d97",
+ "5b0b0f8cdb6c90582302ffcf5c20447206122f48",
+ "0d8b504da88975fdc149ed60d551d637d0992aa1",
+ "507505543772f54342d6ee855fa8f459d4bc6a11",
+ "71abe1781fa182d92e97bf60450026cc72984ac2",
+ "e0efa60318229590103e31c69ebdaae56d903644",
+ "8ad1147dcd02196e3924013679c6bf4c25d8c351",
+ "9679de8286eb0a151db6538ba297a8951c4a1224",
+ "73b9a0e7032a5ae89f294091bc6cbb9a67a21101",
+ "152f846d9f30a3e026530c2087ecd65c39bb304b",
+ "73de3b1233c1da8fd46f9a4bd8ebec97890af9dc",
+ "25d54640c4a17aa342490c4c63c172759361bf56",
+ "eca76e00f897461f95bbb085f67936417ae03825",
+ "802b5b3de0a38e71f07aa3048f532cd1246bc5af",
+ "10d40ab670bf1fa53c925462f84f43507cf3b9bc",
+ "2a14a2ff74f6ec3546b257889949630d3b2a0dbb",
+ "357efe3f93c58bc4a10d40b1301045405b8a9f73",
+ "570430f532b1e98c5d72a759ccbe7851099cee5f",
+ "3174a146b81819fe2cd42e23081cd902ac743a8d",
+ "940873068ea1383f4d962613cc1eca7c8cecc00e",
+ "2116ab332c0bedfd038ad9d39c2e17219abf34aa",
+ "527f9c5ccc6b76ad6e88ca571272a6a2ea535921",
+ "04d21d5e6b71b2634dc67b36bf9b2defce7a7cc3",
+ "37a5a4660941852c298e4caf4592b46b98ce512c",
+ "780be6395b7c65d8d90ca2e1c3c2a46c46c5a154",
+ "6251d68d3039f7b215b205f0e61cb2d732e5bc9b",
+ "1d7efb089db2fe7a60526b8ff50b0c681fe1b079",
+ "1f21cea72f54a6af3b0bb6831eb3874bd4afd213",
+ "cd58e9e1b3ece090edd60a072f66b6cf52bce06d",
+ "fcfd07e6ad0b5eadb0af1bddcc7b04097dacad7c",
+ "e6fdf0f32f49d2a2380f5b458469052c272f8d9b",
+ "324669468c32535f19bc4791fcaa34f2ed82200a",
+ "ba584703bd47e9e789343ee3332f0f5a64f7f187",
+ "17b3541f736df97465f87d9f5b5dfa4991b37bb3",
+ "39e9e45359e20998eb79c1828751f94a818d25f8",
+ "5353ca39fe2f148dab9ca1d637a43d0750456254",
+ "603d37b2a108e2b437bb9b3b2ffb5962b4aa198c",
+ "6000774d7f8412ced005a704188ced78beeed2bb",
+ "537a3281dfefbd7939d27785732a2aafddd3abcb",
+ "92446d8dfc8e57289e6120a7efc6932650ed3410",
+ "eacefc2460e0ac5fe2ad48a9b0ffced5aea451b9",
+ "4314021484adf9b32b3ae5421fac6fe0ed56e53e",
+ "5786699a0cb71f9dc32e6cca1d665eef07a0882f",
+ "2bd4f1921c78c2adffbe2eb01117c7936d0a0789",
+ "de2b60b62da487644fc11f734e73c8b0b431238f",
+ "e752540aeccb620f23c1e2f15c4c707254f6f596",
+ "638ec33f363a94d41a4f03c3e7d3dcfba64e402d",
+ "3fe0bed568c62df5e89f4f174c101eab25345b6c",
+ "17773f342aabf0b177c9e3b8d8396d851cbfe64e",
+ "1ae01f9be1cabf50ee735383a9fc3342e778c17e",
+ "bf76d02e2be0dd8f99f106658ea7cacfa8df69d1",
+ "f82b463a5c9eadb2a6667a1cb51b46d8d8d8d69b",
+ "073e532b7cf87928bcd2512a0faf1151f8bd199a",
+ "0912e12e4c7dc1c87ea8574065725a63342cf19d",
+ "d52b9abcd97f38c81342bb7e7ae1eee9b73cba51",
+ "dc98be5d5390230684a092589d70ea76a147925c",
+ "47bd4d333fba53406f6c6c51884ddbca435c8862",
+ "8ad72fe39fa8c91eaaf12aadb21e0c3661fe26d5",
+ "54ebea0a5b653d3c680131e73fe807bb8f78c4ed",
+ "19d5bfd402f91de0e670ef5783bf5c0a3f5ab478",
+ "659feffdd12280201c8aacb8f7be94f9a883c824",
+ "2b681b3bcddeaa5bf5c2a2939cd77e2f9ad6efda",
+ "30be73c965cc990b153a100aaaaafcf239f82d39",
+ "dc887691eab129c5728e26b095751fcadd36719d",
+ "ddcc8433eb019fb48fe25207c0278143f3e1d7e2",
+ "0ce1edb914c94ebc388f086c6827e8bdeec71ac2",
+ "c6842c86792ff03b9f1d1fe2aab8dc23aa6c6f0e",
+ "5043bfebc3db072ed80fbd362e7caf00e885d8ae",
+ "f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f",
+ "e4ba98f1d4b3c80ec46392f25e094a6a2e58fcbf",
+ "4572d589699f09d866a226a14b7f4323c6d8f040",
+ "bd1a6e384f3cf0f9b9a60e1e6c1c1ecbbee7e0b7",
+ "3363381aef8cef2dbc1023b3e3a9433b08b64e01",
+ "3833ca68f9f42fd11d4e0a036e9a3faae5d5f1a8",
+ "4316d710b6619ffe210c98deb2b0893587dad454",
+ "c22383d089321fd0c58a15c1c6ef5d24b5b5ee0c",
+ "d858f142ea189c62771c505a6548d8606ac098fe",
+ "66d618739859bc75ab9643b96a9839ac7802ec90",
+ "e3aa0be212d7a42839a8f3f506f5b990bcce0222",
+ "d25497d443d0843dbf2973e802c06722f2cb4578",
+ "db2d83bdc0bac7b4f25fc113d8ce3eedc0a4e89c",
+ "b706a216e49352103bd2527e83b1ec2410924494",
+ "4aa0cfb129c36cd91528fc1b8775705280e60285",
+ "4e1cce64b1ec11080a01172a0c296431d9469294",
+ "e3fdd7fa9255bba0a206aea059cf133565c48cbd",
+ "f0f717ed3495ed2e58d96e0084f73db0c7b3ba3d",
+ "a79cf96a15f4b5376fae0024c0b0cd44cfa8a295",
+ "49c3df840c2268479fb8f5cfd7df023bd6927bc9",
+ "9d9d56fcae37f1b3d48d80f8b7eefabd3477569d",
+ "09bfca4ee4f691f3737b3f4f006d0c4770f178eb",
+ "0a1ed0a251d22bf528cebfafb94c55e6f3f339cf",
+ "bc5b0c72a3755de7f3dca9f059aa19cc9d27a843",
+ "a096cfeb58b927dde6b80ad295e564513514f9be",
+ "b1e952300954b6d33911ba29a984455fcc3f1024",
+ "5774f912db3dca1e9049af15cce6a4f7845a173d",
+ "a2f8cf63192ebba929451a221cc382bc0ca5abb7",
+ "13e3663d5878001666981eb5ef6efb22fa6799bb",
+ "9697b9e1667b4f2daa9ea454b4a0e0f905585c8b",
+ "32088dfde15a3f8ad4f2547cb083777afddc12d5",
+ "fabcda911ebc80e3a9b6064863da4f2e5094814f",
+ "2c3591cf5e2f5de644aae09a73a896f0c7964f43",
+ "bce88f90c3341ed14df2ce3919f253334cd834f2",
+ "df7bbc5a4c8304aa8aed34cb67e339035ac2c34b",
+ "c305f6229dde8f3946de5574ac9779309073f2e3",
+ "ad63993db3525be5e290e0ccb3d5122c01bd356d",
+ "24b20d4f91c894e19947389d3040adfc174a6af1",
+ "58c3d2641b48a9db2e29009f42077dcd70f7e351",
+ "8450fb3261e7ec1d734c2b11ca4d875fe82386eb",
+ "9a296a2da46d296f3d0b78d3941ec468c64ba3e6",
+ "bd7b0f03050125e8dd8bd9498e34561e1e88db03",
+ "b6104ad646d672770561918073f1aaacb7c7b341",
+ "8c177eb55da21bee1cd654d66241b98fb0e44c86",
+ "1c45fbaa5f4d66070b7f1ee5e4653aadb14aa97d",
+ "0ed231cd84006f5fdfda7671beae2b9b41a2dafa",
+ "8ed4fee000f82e6248f7f8cfdd11d53fe03f98ad",
+ "302ebf7b124c9a037333a9b81a5f2ce0880f8a29",
+ "eba91bffe866a695d145c5e1692509f92de5b23b",
+ "5a1f4a878b75dbcfbf0d4ae783bf1c1229309470",
+ "5351a31139b9b5e3f8d50252ac081249b1ad00fb",
+ "fdc6f7632078dc5b570f9120d9ab07892e784554",
+ "4a0126da8cf7794e913a13e3f8f4ab62ca5e2981",
+ "4a3df17312a2ab95a4d75396065079aebfb2a1e7",
+ "51fac22c802ae94247664efd95a1e60d138a278d",
+ "c6dd14eb5a4abfcf1c8dbc7187c2ec3b8d9be1f9",
+ "96d70f8f82a534438b938f86b3a6682eb34824ca",
+ "e714165da098686f600d75b914448fdd4a057d60",
+ "60ba0670d68758e893870079916954a7f01afe23",
+ "320e7d1fdbab2bffb8138d66c24724cc24ea654c",
+ "404840df034905ae2b5a9c922639e1d9f694516d",
+ "63f0c49628c9695704d0014409f030d82bc10f70",
+ "68186dc73e3d123999ebb93a6d5a5d0bbb4d4e91",
+ "9ba2ed9f74f5122f25113cd6d5e14fbc442c867f",
+ "993a5608c4942b5b81a6c14fd78779d024e6ed41",
+ "f07ec0309a1e37629f097408e1cb75f7d0ea58c5",
+ "1ffcac9e1bbd3d00db1e2089d8e915f20c0ac568",
+ "9450776e99a5a1b413b98cb095f6fe7f81935c3d",
+ "c72b35c5dea306de35ad0ff207eff4d14b37b880",
+ "5e4e7abcdb8f4101b9aa0ba84658be21c445b1d5",
+ "ef74ce50e19736bf72341a572c1ad6fd2ba6c3fe",
+ "55a266187baa9d1c68447ff6ab404a4324de7935",
+ "8a65a223354726586d95f45aa8f6175ca23b784c",
+ "2ddd12523600e8b80d2be0bc003cd447bd2751d4",
+ "19160c71c598866e9c96af667045c886c8dc9b48",
+ "163372f10bf5f028ccbb122eebc9cd2deb30b094",
+ "566ab030e0a0f010dfe0d185b0804b53817db7ec",
+ "0c74d5b6c2ef578266361a58ec7c848cd844f2bd",
+ "452cd7f4850757ad76710cea53bd9ad8d181d5dc",
+ "98cbe204421b538fd2fbf4a1ce689f8398bd2ced",
+ "b917f21f99eeacf49f55e8fd089b93119c7dbd9b",
+ "bd4d8f4a02886a26b60c76048547a453691fcec3",
+ "1dcf1de382a0bf95a3d8b0849546c88bac1292c9",
+ "799748e42a644db85394db066af658809f89c523",
+ "c693557ee87e311340eb0f8a811b8bca027af421",
+ "912b86862ad070dd3d21f51e05e361eba1f515da",
+ "67b085271fd9cc0a61eb04fcaf288ad35b2e7995",
+ "a41a8b5641dad26c7601ea93818611b4a6465058",
+ "5ede807d3bcdace2e25d5614382bfdf1663012e5",
+ "8275c3b8829eb16a54fb49ceda2f6fbb44546c26",
+ "5a2b47396587b499575782b60cb223a830bc86d7",
+ "e113ac14fb2b70c1510f92ea2a0405ba4da01f5c",
+ "10e53fd4d987e37190432e896bdaa62e8ea2c628",
+ "286c93b65ab3c3a0a257b0a6ebdd99c06c674c88",
+ "7b93e7e3c64b837b69da7497fdf4c28b677625bf",
+ "0bc23b2c7e6419d3cd7e108d6942b9431bf5c25c",
+ "0a5f0e4a16f5b12cde3df1ea413852aeaf176176",
+ "44984c2480ac8aaef4a660a06565aa76c577238c",
+ "5878d0f20e7cc521a437217dd21c3a84788d3a53",
+ "73120785e720701d1142d97bdc72bf5d6b5af4bd",
+ "fee8f41ab7f59597e35d8a6eb01b9edc9b04d51e",
+ "e0feb1bd93ad9fb1e064706cff96e32b41a57b9c",
+ "3dc8cea436c52d0d248abe9648b0e4f1d02bd500",
+ "3e224b1b9e18dd28c89a764b1feea498ba952579",
+ "09d6cbdde6ea3469a67601a811b4e83de3e68a79",
+ "3251b36ee9e9c3effe3293f8d7094aa3841cad55",
+ "252e267acf720ef6333488740a696a1d5e204639",
+ "789cafde696403b429026bf19071caf46d8c8934",
+ "4a4f88c5e13143f882268c98239fb85c3b2c6cb2",
+ "8046b9d6b423f24457cfb20210d0ee8abc98e22c",
+ "bb4eda2c61102759e7c03ab12ff7c19547e20cbd",
+ "52da76c0c8190be88281aec828efd44df176ab34",
+ "878e2200222f5e11137d5bfde325a5db30687592",
+ "1789190601b7a5361e4fa52b6bc95ec2cd71e854",
+ "44f8a2b2c0dfb15b4f112e22de76d837b89bd4d6",
+ "aaf681a518ce5c9a048328b86ba5b9c5123375aa",
+ "dbd77d2e6c54ed9fafc83f1cf6f48342250996d7",
+ "532fd1449686690273222ebd5cb86c233ed19f58",
+ "60de19e6c8e44b1a78acb0dd73722b2feaa7ccfe",
+ "85289261815e7d2fb1472981652fce50ae8cfc42",
+ "71b610fca525744bc70eb96c9f9113cddbc38f4f",
+ "2977cca2c82e3c5336805ebb6226c14137585b54",
+ "1d2d1a5ed9cfc58d0a7bdc1d9dda5ecb9987da9a",
+ "d339db49e637d2a8122a67ad846a294124f1a2f3",
+ "02f16015ed9e2689e10f86f1b7c3522e541c0c75",
+ "0e7eab15d6b4184921b82fbca6f89dcb60ea972e",
+ "362e2295d95b2e2797457760eef1f172d07d7417",
+ "c067143934cb76530adbb8fd4e2df1ab737a16e0",
+ "b3add478d4382b78ea20b1671390a858002feb6c",
+ "907df2bf39d70510951b7bafbf661f286eed90a5",
+ "6e5d51a72d142f2d40a57dfb897188b36a95b489",
+ "00f6db9a5e6fe2374b5a494b08388c2b6e0792d8",
+ "eeb69005da379a10071aa4948c48d89250febb07",
+ "af799dd7e23e6fe8c988da12314582072b07edcb",
+ "3b27257997ac51b0f8d19676f1ea170427e86d51",
+ "90ac2db772d9b85e2b05417b74f7464bcc061dcb",
+ "451bc97f7519017cfa96c8f11d79e1e8027968b2",
+ "066aaf67a580910de62f92f21f76e3df170483cf",
+ "d5e162564701848b0921b80aedef9e64435333cc",
+ "12ac6f103a0ff29fce17a078c7c64d25320b6165",
+ "21f7a9a2da446f1e5b3e5af16ebf956d3ee43ee0",
+ "81065531e63fccbe85fb04a3274709593fb00d3c",
+ "09ca864bec94779e74b99e84ea02dba85a641233",
+ "d635e3eed4beb74213489ff003ca39dbe47ea44e",
+ "b75e5e9feb70f599a6f6232e71bd5b0030608179",
+ "02419d851c01139edf9e19b81056382163d9bfab",
+ "57b7ba0ca94313c342b03bd31830fe4a8f34bc1a",
+ "a68959c06e5f8ff45faff469aa16f232c04af620",
+ "70b332574395cde2c56db431b619be9823407aed",
+ "45c3bb7696f29655189abb78ec1c97f511643159",
+ "99a1348743f3550dd4524408725efab8eb319960",
+ "acc766c65cd4e94a5e1fab6a2f85148dfc8613d8",
+ "28bdcdcceb92a1ac450b8b6a3d3d0627d839054d",
+ "9ea12cb2c426d521b7c4cad5b02ce18e5b614d4e",
+ "8347ef8861b75bbffcaebb706a0ae296daabc20e",
+ "e844c4278ecba985c08e0dea1181343a07c04c3e",
+ "4b151bcdfe290542f27a442ed09be99f815f88e8",
+ "1d7200e19d1ffdaf6927ff0be701724c85be07d7",
+ "1861e32c3c484a1aa5f55ab109e08dd1b32c6fa2",
+ "34c56f43fd3255fc239ffe33d0fbfb8195be6a24",
+ "e5f6cae5ca7ecaac1ec2827a9e2d65ae2869cada",
+ "0c900514d3446d9ce5d9dbd90c21192048125440",
+ "56b53c8f4bcdaada801d311cf2ff8a24d6d96883",
+ "de748cf874e4e193b42eceea9fe5574fabb9d4df",
+ "4bafcb5aacb1abc193698a13ae99394e09a25101",
+ "687cede1a44f70c7741abfab6ee2aa53dd2bfb54",
+ "34d8332b975f9e9a8298efe4c883ec43d45b7059",
+ "698bd8c759ccc7fd7398f3179ff45d0e5a7ccc16",
+ "2872764df7b4857549e2880dd32a6f9009166289",
+ "164343da11db817e81e24e0d9869527e069850c9",
+ "e1c6222f2fa8d05d7825d8e9af7b9bef089c0b5e",
+ "127fc12785c42eeff7da15abca690655add7c710",
+ "53c041061964825372701d75b96a67e82bc3b6da",
+ "ba4bfac0366399080e878cb5c41023c3eb7f7328",
+ "61ad4ef7f9131fcf6d25c34b817f90d6da06c9e9",
+ "4a3ee4146a90c619b20977d65951825f5675b560",
+ "67613a3a83092588b85b163b9aeb3e87ec46b4ea",
+ "c197c86ceec7318b1284bffb49b54226ca774003",
+ "ba035118bc8bac37d7eff77700720999acd9986d",
+ "c85270e307e7b822f1086b93689124b89768e273",
+ "2042461b754cd65ab2dd74a9f19f442b54625f19",
+ "04669a54b799c105572aa8de2a1ae0fe64a17745",
+ "48d6674adb5a077f2c04b42795e2e7624997b8b9",
+ "15177b3e1c91529ba02c056035d71463f9e66a03",
+ "241aa26c61f638b7e76787558bb1be49984f2a0d",
+ "4605d2f4267388d02d810a3cea448a48371435a3",
+ "d1cce505a1dafd5b5d842ec8f91105ccca7d5e4d",
+ "0a10ae77a57942352f3d2abe5d58b199b1f83d33",
+ "dc6c49c40b1d5acf3ee58784ec34360806f48a22",
+ "6d9393fd0ea1e1900451b5ee05351523725392bb",
+ "8a603c50591cd2ba1039afa3f28540b0f43c82c5",
+ "b4bd511b8cff2cfd83faf37f48b6e256dabcfc6c",
+ "c02f6844c6d27c8357257eab162250b54d38390b",
+ "46cb8029e744ac128d4ce58c944ab21ab7ef3e25",
+ "e27eb84680badf95422bf5798faf8b0b7fa332f4",
+ "a974a4972c0ecd2206b045e387bea4713a5451b6",
+ "e3480072bc95c202476ffa1de99ff7ee9149f29c",
+ "74548703f9851017ce2f556066659438019e7eb5",
+ "562a587face36ec7eff2db7f2fc95425c6602bc1",
+ "f3cd84cc45f583a0fdc42a8156d6c5b98d625c1a",
+ "f48473482c0e3e714f87186d9305bcae30b7f5cb",
+ "288f60226c596849c3c57926e8421c83b5abbe87",
+ "5eacc6522521f7eacb081f95cee1e231648461e7",
+ "5eea182d6651a7257bc8c3614507e1540c766fc2",
+ "8d49996a4338670764d7ca4b85a1c4ccf7fe665d",
+ "48e3b9cfc10752fba3521d6511f4165bea951801",
+ "0422d3543c01df2f1d8bd1f3064adb54fb9e93f3",
+ "878a02f3ab98d37206c9852c025a46b86dc882d0",
+ "c8de82b962142a5f3d408ecc3920642b166de028",
+ "bf744c1e2776ed1de3c55c8dac1057ec331ef744",
+ "85262acf3ca9816f9537ca47d5adeabaead7cb16",
+ "934c04d3cfef185a8008e7bf34331b79730a9d43",
+ "60a59edc89f93d57541da31ee1c83428ab1cdcb3",
+ "935151eb71beff17a2ffac15dd80184a99a0514f",
+ "3cd63d075497751784b2fa84be59432f4905bf7c",
+ "8531ad5ac454cc2deb9d4d32c40c4d7451939b5d",
+ "3758e8c1664979749e647a9ca8c7ea1cd83c9b1e",
+ "dd6dda9da676a54c5b36ca2806ff95ee017d8738",
+ "40fd4d696c55793e996d1ff3c475833f836c2498",
+ "ef31541dd28ae2cefdd17c7ebf352d93e9058c63",
+ "d877e195a05aca4a2f1ad2ff14bfec1393af4b5e",
+ "39e8f0d32258f304928b29bc7e1f7d85fa5ae218",
+ "f9414e9ed1a16132c5d3467991a3bebc4367a1bc",
+ "984a623e0c0dfec82cb1cd390ee1aab51fa02bee",
+ "d60d3f8ccefd848d551d25bcb7f3e9251333648b",
+ "e9cb36b5954d1af1593bbc4fecadfa5cb170bd44",
+ "058e7a538e020b73871e232eeb064835fd98a492",
+ "6f14738ec2e9dd0011e343717fa624a10f8aab64",
+ "7aaae28e06aafe63ac94f7c6dee81135b815db92",
+ "9b1f3cf3fdd02d313018f1a67c42106e6ce9f60d",
+ "21c5319c82ca29705715b315553a16f11b16655e",
+ "fc5cdccdeeedb067b8b2a3c7df2907dfb7e8a1b9",
+ "44df9a1310c1278b62658509aca3ca53978e8822",
+ "fbdcb39db6a6976944a621fe11bf1d2ff048d7c2",
+ "1a01a2a1218fcf9faa2cc2a6ced025bdea687262",
+ "056dcc8480ecd2c03ec004aa76278d1f2d621561",
+ "b33d6d9045da8f0b317162facabcd1dc9ebf751d",
+ "be8f9b519d692dfd1e2726ee9e26573dabc99e70",
+ "e522a857d234e5ade679abfae807bcf4ecdd6f2e",
+ "533e7cbc5efc1d58d14cefb68904cd3af47fc316",
+ "fdeb0e5beb9ddbfb49b4aec3daa55d71e0cf1956",
+ "966952ede72900ddaa20888beddc86a5a002cbd3",
+ "b22ab0397e893d9c092ade34a8e826ef576b285c",
+ "65ce500f7cad946ce0e172c6ce90319caa29e787",
+ "c8645a939af24f227e4123b89af14264176f7c60",
+ "62bbe12f1a737d9b96358a9964466ffea6a6487a",
+ "696cf9f9160e3a0ff208db614af118915cbdcf2d",
+ "2f2695de1ae62d84ca8336c7e6ddedc80aa2e521",
+ "59d5e3e86e2583fba0cf04d4f126a5205a24c4b6",
+ "d60bb33b97b968b555ce829961d41971ff826415",
+ "1200e7ebeedbe0d10062093f32925a912020e747",
+ "88e9a306715e9379f3122415ef4ae759a352640d",
+ "0a1cb8dbe71b5a6a0288043c3ba3ca64545be165",
+ "a240efb690601cb1ef02a6778a23e450559b0bef",
+ "0e7c45d7667feb56e5664247a882451c3d438def",
+ "eaa7ec646db93f0096ca8100c361018c2608319b",
+ "cef76bca08c1f437150890d1b8bf430a66ebe42a",
+ "21743fe8af7bb684d5ebbd4075f397fccc30d158",
+ "006936bbd6c5b235665d87bd450f5e13b52d4b48",
+ "698ce67b5e58becfb4ef2cf0393422775e59dff4",
+ "a698fd936b13588c6747b182bcfdc13885a8ca43",
+ "357a3836bb5da16f314f3a1e954518e5468cd915",
+ "37fe2217f577b0b68b18e62c4d17a8858ecf9b69",
+ "5e303a03d04e6788dddfa3655272580ae0fc13bb",
+ "c9ad4a0850ab676c5c64461a05ca524cdfff59f1",
+ "cc5888f14a5768f254b97bafe8b9fd29b31e872e",
+ "63f943103f250ef1f3a4d5e94d145a0f961f5316",
+ "516c03b21d50a644d538de0f0369c620989cd8f0",
+ "25ea2e8b0c338a877313bd4672d3fe056ea78f0d",
+ "59033da2a1afd56af1ac576750a8d0b1830d59e6",
+ "2ca09f0b36ca7d71b762e14ea2ff09d5eac57558",
+ "3ff3baa0074445384f9e0068df81fbd0a168395a",
+ "4c7018119aeb66335746e6748456c821e304d3a2",
+ "75a75c47eb912f3fd06df62a9e4b3b554d5b2bec",
+ "2e617bd795b3b55b2ec23543721665a2b1c77b9c",
+ "a0f0c4de6cc321130252e86658c21b2e1b6af008",
+ "7868b29620b92aa1040fe20d21ba09f2506207aa",
+ "a82d2503e718d17628fc9b4db411b001573f61b7",
+ "e358016010b6355630e398db20d83925462fa4cd",
+ "82357e97a5c1b505beb0f6c227d9f39b2d7fdde0",
+ "66eab4bbf91fa01ed4f72ce771db28c59d35a843",
+ "eb91bc9b9ff26bfcca077cf1a888fb09e8ce72be",
+ "c56ffb4a6541864daf9868895b79c0c33427fd8c",
+ "1e39adf7c3f5e87695789994b694d24c1dda5752",
+ "93d37f677addd2450b199e8da8fcac243ceb8a88",
+ "d54a9712c29c4e6d9d9ba483fad3d450be135fff",
+ "a4c3885fa656a92508315aca9b4632197a454b18",
+ "4c1fd1f78ba7c16cf6fcd663ddad7eed34b4d911",
+ "389b730dc4e454f70d72ec19ddac2528047f157e",
+ "7d1b5b69a5ea87fb2f62498710d9d788d17beb2b",
+ "b8af3fe6f1ca88526914929add63cf5e7c5049af",
+ "dafaf2c27f27c09220cee312df10917d9a5d97ce",
+ "7473b8cd3c0ef9932345baf569bc398e8a717046",
+ "67f57e154437cd9e6e9cf368394b95814836ff88",
+ "3c2af9d14e43d46b541ac1a0cdd7be4980aeed84",
+ "aec7142dafa1f96154018eced507854bb544cf41",
+ "319d3da49ca42fca687de2accef1d22fba786405",
+ "7577f792244cae44227e675aafcf6597a2eeb00d",
+ "9df166a4a89314f5281b37e524bb366d7ffadf23",
+ "a0b8af84c1ddf5d9dd7d1eef8a19df527864e8cd",
+ "ba91c4fa57b566baff698a3354db2b8af8626d3c",
+ "7d5c94b0fb6384b91d963d6d398468d96bb4983f",
+ "4c54840ac217908029e77a96336c03901a6776d5",
+ "6ac92abcc06bb8d52d8179889c6b1173ba7bd027",
+ "8bc95edbea781cc09dc6755f570b72a993df1679",
+ "a9992b9918cd8582e2fef748a61ec1c46894b13f",
+ "bb8b60297070d7c352a06c6bbc3854cfac26d46a",
+ "4185a5807b9fdbdcbee813f8e82ceb433ad75c68",
+ "9bc1a58a9472452100f873059683a9a37e37063c",
+ "4d1701b9c993f5edfd232ea06a6f8ba540113b59",
+ "5d5c7c1b342c89b4b0d28cd99572827cbe3e6f15",
+ "d641ffaf2a3a84c8c85d24850b916c5baf547a38",
+ "c5bf5e88b285eda01f3d2044c76a6f5651dfa4c0",
+ "6b7aca9462a226dbdef319e6875523e322c6f80b",
+ "b9740e040f5fc06920f38d9fafd966bfd3cabe1e",
+ "830a7d5e13edb5f6f81c36877edf68b55a4182fb",
+ "751030d0b6c06337bb2870cd174ec83ed8417b3e",
+ "b8957915e5d02d9e341eaa07a90019aeb90d546f",
+ "82cfcd48e0c239ddbdf4fa122b8715275b761de2",
+ "97c73ecd70bc7e8eefb26c5eea84f251a63f1031",
+ "5d1abb695642e88558f4e7e0d32aa1925a1fd0b7",
+ "0e5af3b6dc164eb2c699b70bf67a0babef507faf",
+ "f08a912ce02debbaa803353686964b3c5fcfdb53",
+ "8625e8f9b6f49b881fa5fd143172c2833df1ce47",
+ "b421526c5f297295adef1c886e5246c39d4ac629",
+ "9be9bb9b3a1638dcd948edd6179bd8ee4ffcc137",
+ "bea6fede6328fabafd7e68363161a7ea6605abd1",
+ "7183a25510a02ad00cc6a95d3b3d2a7d3c5a8dc4",
+ "af40e34de7c30e4fef253024a88257f6dddc547a",
+ "d2cac68225a25a5486ea848af95680573ee3d393",
+ "d952189f6abb148ff72aab246aa8c28cf99b469f",
+ "87fa769912b1f738f3c2dd87e3bca4d1d7f0e666",
+ "79d7792942fa009316de2d7d1a4d7e8b33548947",
+ "6604030f7da573a8c00641f9c7deef6c143b6022",
+ "2746f9ec96f9ce3a345b11f03751136073f7869f",
+ "f73773fc39d43df7661609b9f7a733ddfd091af7",
+ "be8a20787124cf52c56c5928ef970df2d8a26f51",
+ "8ec1dce97ba5b616e165068225bba873179482e9",
+ "dff1e225fe6bfdf7853663bc48831e9714bf035e",
+ "ebe2549568386d5c289ec0eb738172f1a0445259",
+ "9d5fdc88f91586bf5d1afa13b9a77302c39b5e7c",
+ "ab7c7c3c823cb2f8fb1b54fdc82b3e133e8e8344",
+ "bc34429b8d1a620c58639f376bee9ba425a035d3",
+ "0bc8d9f00bd34806bc82d01390855ef9dcbea85b",
+ "a1e978879d35af3590549437b80679b5c00f27d6",
+ "1000c919125bb13f265b101341c34bb5af814fd3",
+ "488e5cfdd4d2d30b161fc45819a82a6984eb0f99",
+ "4b986a99445e49ea5fbf5d149c4b63f6ed6c6780",
+ "64485a221d9095fc7ab9b50cc34c6b4b58467e2e",
+ "bf9e9aea47c3d112929fc56abd75a48d31914fda",
+ "73334ff5470db03e5b793ce1d5854642b2c21799",
+ "7fd8a65d950d0e77dd39cc4ce2776ff9673ae470",
+ "d4c0da647de59c9ccc304a112fe1f1474d49e8eb",
+ "ccf79a1a63ef35de038a4226a952175c4e9f4f59",
+ "5fb53c92da84ebeff403414b667611d6bcd477cf",
+ "ef5bccf2a7a22a326c8fe94e1d56f6f15419bedd",
+ "311d38cf15ec7f5c713985862632db91b7a827af",
+ "e2d5e96ea4bbd4fc463dbb76d07dd8aefac05e3c",
+ "61625cf2338fe84464c5d586dbba51d4ff36a2b8",
+ "9d8cd3ed749f2c2f846e0c58c485c8a0d5d5181e",
+ "2f23beded3e46a3552fc3c1a0fdfb810c24d8f97",
+ "54bc99d2b886a868d79d537ee5e7829bb062fbe4",
+ "52fd60d5dc3f0fb3ed5c19b63f6f2312cd1f6add",
+ "8a8ef1517d27a5b4de1512ef94679bdb59f210b6",
+ "f6ea1e9c0a0acf137a8a4c5353bc97ead6b82cf7",
+ "ea93fbd2137c797ed8a686737e8bdfeead20f1b1",
+ "18ed04a0e502896552854926e908509db2987a00",
+ "2a9d06026ed251705e6ab52fa6ebe5f4f15aab7a",
+ "c2ef6018eecde345fcddb96e31f651df16dca4c2",
+ "93cc78652ed836ef950604139bfb4afb45e0bc7b",
+ "dd44733e94f3f6237c896f2bbe9927c1eba48543",
+ "ed90430e545529a2df7c1db6c94568ea00867a61",
+ "3ad0af28e408092f0d12994802a9f3fe18d45f8c",
+ "9da10a9f72e3f87e181d91b525174007a6fc4f11",
+ "d186a0be320e6a139c42d9b018596ef9d4a0b4ca",
+ "62b6a5dfee2e22ab9015a469cb68e4727596fd4c",
+ "84ede759015e7480ca8e6ec2af6e2f596aa92dec",
+ "f3085568e45c2ca74118118f792d0d55968aeb13",
+ "84d160a3b20f1de896df0cfafe6638199d49efb8",
+ "6915c9c6966bf1482ac93236453013535e8c5d80",
+ "bd1236bebd1ee50c8d9206e69b1986fac9532a49",
+ "70cff2bc010d0c047cf5b167b2c600e42f6863ab",
+ "6610ef4a025fcea2a5958724b9493a1b081b8f66",
+ "ca018bb3db661230fbf51bc2b3b1559ac7987040",
+ "313e89f2da215f0dcc54a638c5749c4ee959e74a",
+ "c0dc8a542fd18d372a2ee67a203f2cfe0a345a05",
+ "b31c6944d9cfd596b6c25fe17e36780bfa2d7473",
+ "3a7aecd4bcaf75c7b0b02c26ea6ceacf3e8f5f4d",
+ "1fd80f714c85ca685a80f32e0a4e8fd3b866e310",
+ "baf7b939ef71b25713cacbe47bef8caf80ce99c6",
+ "118f166726472bd5b5578817503ed0992c9102e2",
+ "4c62b2337352073ff41fa9a9857a53999d41b49b",
+ "3addc6860c44edcf28c262489f23276b84e11812",
+ "0df31f1cd96df8b2882b1e0faf4409b0bd704541",
+ "a224b43863a0679f153bec24e1d329c63f1ed234",
+ "700f71ffefd60c16bd8ce711a956967ea9071cec",
+ "fa9a2e447e2cef4dfda40a854dd7ec35624a7799",
+ "6d62b9b4db6228122a5f1cda81b06f156afb04ba",
+ "14f50cd1c2f5d29d9b070746c1fcae59b68ca26b",
+ "d3e1ce1d2b3119adf270b2d00d947beb03fe3321",
+ "2f4525d4a200e97e1b87449c2cd9bd2e25b7e8cd",
+ "b0b14b3d12980912723fb8b66afb48dcda742fcb",
+ "bc28b5a964c8f5721eb58ee3f3c47a9bcbf4f4d8",
+ "49b64e09d81c0cc84b267edd0c2fd7df5a64c78c",
+ "8bf9683c80762d7dd47db12b68e99abea2a7ae05",
+ "5600569133b7bdefe1daf9ec7f4abeb6d13e1786",
+ "66a60c7201c2b8b20ce495f0295b32bb0ccbbc57",
+ "3c13fc5715231fadb16a9b74a44d9d59c460cfa8",
+ "c05b6b32b69d5d9144087ea0ebc6fab183fb9151",
+ "b7ce164e9e75be4b5eb42fd89c9c53ebecea6729",
+ "abc7bac20e8b15d5aa38d1c9af5bed4e0ffc7748",
+ "928c299530ecce5c25dcf62f72f6aa901d6baea4",
+ "87b2ed1c62d42fd9fbbd154095f2387c6a18f880",
+ "67336cfb9d93779c02e1fda4c87801d352720eda",
+ "074b9950a587f53fbdb48c3f1f84f1ece8c10592",
+ "132630f17e198a1748f23ce33597efdf4a807fb9",
+ "00b5ec860e174d7a2edb2b46523cdc5401513cbd",
+ "3b17774c8087e239542afe1c7976c16c5446af26",
+ "cc71779727e9051e59c8a242b4157fc1d3172caf",
+ "9aab69982e4a9b91a86743f73dc48db30daf9265",
+ "ff0cd44f590a80c5c87aaa85a0d2bab2d350bc4a",
+ "7b6c7f676d78f988b01e9841ab18d389886ffa26",
+ "5fcf76dda71647a65d6fafbab2bf03065bf3d52d",
+ "c63eaf104979cae41c9c5b2ef1a9bbe5d1c05480",
+ "a7efb5dce8081d1d96445355d55f05e6e825d41f",
+ "89223f29832931516d6c1f00a9ef2263b8674f5a",
+ "5506a7066998a2be47b86e28d061863a475a7ca8",
+ "a2e83c6e6ad2086f97277540d9d9ef4aebb74a28",
+ "da50b1b4177cbadb977d52aa70011713f37a2156",
+ "e7d90c28cbbe26b9a31fa8a136c209418ca3c9ba",
+ "0ce981aa4a840f84b670bbb3dfd77cd3be87ca84",
+ "1c973b3f5c13399e1194724abe421e230c572206",
+ "9074f509fbc3df3ad104eca5427d03eece453246",
+ "34994ed5371f31eeaa68b294d7f729934280a733",
+ "57001fa0b6622767e63c1b9cd2e6db666d180caa",
+ "6a999a46cb630f44f1a77ac39213fad57a8c1492",
+ "2c3fe5d2e5941e50947ff59c50d201d3968fac02",
+ "1149e08b436cca632ddfe8cee39918f23b50dc6f",
+ "0b813b7539fae6550541da8caafd6add86d4e22f",
+ "ef65452adaf20bf7d12ef55913aba24037b82738",
+ "b260ca7a23bb0d209771db7aae35049899433fe3",
+ "b4ac9780b37cb1b736eae9fbcef27609b7c911ef",
+ "86ed42574cd68662b05d3b00432a34e9a34cb12c",
+ "a483da1de9cb174ca327059e9fd8432b0e8666b3",
+ "6eb2c27f1b7d048a6912a42a0637e470cdc46562",
+ "1d3f5d1fd272883cbc26f3d7fcf9ba58f66d48e0",
+ "5204ace0d7b8410a5fb73c17a20a69e616215131",
+ "60164baf43273401883c7c0b53b0bc4359b9e94f",
+ "7fbf34d79ca897acf21061c2e24b607b090be1c9",
+ "5ae5c9ec39930ae9b5a61b32b93288818ec05ec1",
+ "f90394c695d47b16f608be5366373eec768597f1",
+ "e0d6c62cef4929db66dd6df55bee699b2274a9cc",
+ "fff73bb736a3ebf11974ba2ded176f16a1976f0d",
+ "a4ad886bfdbd1a872bdb3b25a9893994b78adf11",
+ "010245305f4faef0ed473552a58f83d281754e77",
+ "b82b13e45d9372296362e0d6dc481f6a0f2ce0c7",
+ "e6396ecb39ea2c91dc9901213da1d29b8ae1798c",
+ "1ed09f94667962983cce7ec6c7a1df5c0881e08a",
+ "ae1536faa3401b0c62f93e29fef9ffcf134a616a",
+ "a26d3c16f32cf21cbe24c0d7dc37132c407608bb",
+ "d716952ab58aa4369ea15126505a36544d50a333",
+ "2949632c1b4acce0d7784f28e3152e9cf3c2ec7a",
+ "323964c36556eb0e6209f65c1cef72b53b461ab8",
+ "3864a1320d97d7b045f729a326e1e077661f31b7",
+ "6f29a4f68e4156358f64f6a060c5e55ad42f5231",
+ "e1e99c956a36e619398f9e94d775f51a85c26770",
+ "f4d24aa8fe81caab2420dcf4cf9ceb139394b535",
+ "f9d9e55d1072d7a697d2bf06e1847e93635a7cf9",
+ "df7dc4df69114c694956a0ac537119527ecf1b9c",
+ "35e36c0cafbdc3395fd4600a05c613d3073c895e",
+ "24d091b80d513846293c00350f46d85f71797aff",
+ "f95b25589b40b5b0965deb592445073ff3efa299",
+ "6a93ee522c52f5bd54140b6fa0be6a503e00dc96",
+ "657d341c197d036dc27d7f8f5b61c6ff6a678df4",
+ "cddd306a5010eba20a133b6473d9e8d967884f57",
+ "0d825fd2e9e4dd42ac14d5ae6c7f92cbe63de009",
+ "8bd7794fbdaa9536354dd2d8d961d9503beb9460",
+ "e349501d4275363646a099e1d3baed064aa2eca5",
+ "151dbcd21c9ed6b03960a5f0b05c255c9f955618",
+ "28b0eaf7c500c506976da8d0fc9cad6c278e8d87",
+ "a09a8c790a20309b942a9fdbfe77da22407096e6",
+ "2c23f53ca22d7d8885fc4522ddcadcfe7f01a783",
+ "65935d9855ece6f85c21ad38634703d0917bf88c",
+ "dec00ef7c6155c4ca1109ec8248f7ff58d8f6cd3",
+ "cc3d2b7b7cb6f077e3b1ee1d3e99eb54fddfa151",
+ "009d724771e339ff7ec6cd7c0cc170d3470904c5",
+ "e64aea8b539905fa92fad0e7cf73ffa4375f8b32",
+ "6c62681a2f655b49963a5983b8b0950a6120ae14",
+ "db708f7d959dee1857ac524636e85ecf2e1781c1",
+ "2cd0a87ff7df953f810c344bdf2fe3340b954c69",
+ "3aab2116756442bf0d4cd1c089b24d34c3baa253",
+ "3af797a25458550a16bf89acc8e4ab2b7f2bfce0",
+ "235a7e571b33eda1a81e0f73a3173ef95dd020e5",
+ "50d0390056017158bdc75c063efd5c2a898d5f0c",
+ "4205e3cf9c44264731ad002fcd2520eb1b2bb801",
+ "53fc648efc0c82b1e0cc806ab7abf7dbdf532273",
+ "faa8ba85d503da4ab872d17ba8c00da0098ab2f2",
+ "7687a145717677e64300adeb44ac29d90f844b59",
+ "814ec05f3683b661166055a23e29dca0300cd58c",
+ "351719631846db88eb3daf690fca5399aed3fd77",
+ "49c100caf72d658aca8e58bd74a4ba90fa2b0d70",
+ "8cc35f73da321c29973191f2cf143d29d26a1df7",
+ "a3f7325c52240418c2ba257b103c3c550e140c83",
+ "7bb85ce2cf23af5b2d7467c2825fa2d0330ec5d5",
+ "6fe2e3bb57daebd1555494818909f9664376dd6c",
+ "1c63879e1f630e44ad8d2245b8a28a088f387e7c",
+ "313913e603eaf3bb2c3b05079046ec07bb61f8c6",
+ "4f062ad1aebb1255b84c851d00694cc7949de832",
+ "887697058d8464462e8fd6d23c8461e90aec8c08",
+ "2ab94758b0276a8a26102adf8d528cf6d0567b9a",
+ "5397c9a02f77744da25d4ef63a7ebf01affeca62",
+ "d6adb54fefe72482ed049f07af31ddf2c287345f",
+ "4c65b7b43f3fe31350f74cb7d0b2461e111e8dd0",
+ "e6feb6b7c06600924e8b6bda3263c870cfb0a447",
+ "a09d2c48d3285f206fafbffe0e50619284e92126",
+ "925720c5d40c4ebf8601e06025e1402251ef71d2",
+ "611b82d4c4b4f67cc3d83cf0697ec660fcee2fff",
+ "dfd5101b17da36c32ae024b984e0b72712f01a35",
+ "68f1af10052713fda01bfb1e5b831dcf6d826ab2",
+ "e2133b723d0e42be74880d34de6bf6538ea7f915",
+ "e40429d9dd849c5fe0bdf97062b1d9358d99826d",
+ "0ac2d2817d649e3203a8f7c93e7c65be0ca9662e",
+ "7ef25e94db74d85fa7e9271b064a3c7d9ef7add5",
+ "3e05dcce371d3f672feba29f086ad78a93ae3996",
+ "16b9f8ab972e67eb21872ea2c40046249d543989",
+ "c47579857bbf12c85499f431d4ecf27d77976b7c",
+ "1ea4bec1a921180164852c65006d928617bd2caf",
+ "d3ebf0f291297649b4c8dc3ecc81d2eddedc100d",
+ "0ddae73613ab823639de096c287ea6142749f340",
+ "6638e37b887b5a279044afbdc9928e19f678eb2e",
+ "de7b8a41bbe1ccdfc009de51fa6d160db3ca8025",
+ "f52de0603f31798455e48bd90e10a8f888dd6d93",
+ }
+
+ for i := 0; i < 5; i++ {
+ t.Logf("Iteration %d", i+1)
+
+ for _, sha := range shas {
+ pkg, err := ms.GetMavenPackageBySha(ctx, sha)
+
+ if err != nil {
+ if strings.Contains(err.Error(), "no artifact found") {
+ t.Logf("failed to get package by sha: %v", err)
+ continue
+ } else {
+ t.Fatalf("failed to get package by sha: %v", err)
+ }
+ }
+
+ // log human readable timestamp
+ ti := time.Now()
+ t.Logf("Time: %s Success: %s:%s", ti.String(), pkg.Name, pkg.Version)
+ }
+ }
+}
diff --git a/grype/matcher/java/matcher_mocks_test.go b/grype/matcher/java/matcher_mocks_test.go
index 4bdca7800f4..c781c7618ae 100644
--- a/grype/matcher/java/matcher_mocks_test.go
+++ b/grype/matcher/java/matcher_mocks_test.go
@@ -1,75 +1,57 @@
package java
import (
- "github.com/anchore/grype/grype/distro"
+ "context"
+ "errors"
+
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/syft/syft/cpe"
+ "github.com/anchore/grype/grype/vulnerability/mock"
syftPkg "github.com/anchore/syft/syft/pkg"
)
-type mockProvider struct {
- data map[syftPkg.Language]map[string][]vulnerability.Vulnerability
-}
-
-func (mp *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
- //TODO implement me
- panic("implement me")
-}
-
-func (mp *mockProvider) populateData() {
- mp.data[syftPkg.Java] = map[string][]vulnerability.Vulnerability{
- "org.springframework.spring-webmvc": {
- {
- Constraint: version.MustGetConstraint(">=5.0.0,<5.1.7", version.UnknownFormat),
- ID: "CVE-2014-fake-2",
- },
- {
- Constraint: version.MustGetConstraint(">=5.0.1,<5.1.7", version.UnknownFormat),
- ID: "CVE-2013-fake-3",
- },
- // unexpected...
- {
- Constraint: version.MustGetConstraint(">=5.0.0,<5.0.7", version.UnknownFormat),
- ID: "CVE-2013-fake-BAD",
- },
+func newMockProvider() vulnerability.Provider {
+ return mock.VulnerabilityProvider([]vulnerability.Vulnerability{
+ {
+ 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()},
},
- }
-}
-
-func newMockProvider() *mockProvider {
- mp := mockProvider{
- data: make(map[syftPkg.Language]map[string][]vulnerability.Vulnerability),
- }
-
- mp.populateData()
-
- return &mp
+ {
+ 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()},
+ },
+ // 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",
+ Constraint: version.MustGetConstraint(">=5.0.0,<5.0.7", version.UnknownFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: "github:language:" + syftPkg.Java.String()},
+ },
+ }...)
}
type mockMavenSearcher struct {
- pkg pkg.Package
-}
-
-func (m mockMavenSearcher) GetMavenPackageBySha(string) (*pkg.Package, error) {
- return &m.pkg, nil
+ pkg pkg.Package
+ simulateRateLimiting bool
}
-func newMockSearcher(pkg pkg.Package) MavenSearcher {
- return mockMavenSearcher{
- pkg,
+func (m mockMavenSearcher) GetMavenPackageBySha(context.Context, string) (*pkg.Package, error) {
+ if m.simulateRateLimiting {
+ return nil, errors.New("you been rate limited")
}
-}
-
-func (mp *mockProvider) GetByCPE(p cpe.CPE) ([]vulnerability.Vulnerability, error) {
- return []vulnerability.Vulnerability{}, nil
-}
-
-func (mp *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- return []vulnerability.Vulnerability{}, nil
-}
-
-func (mp *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- return mp.data[l][p.Name], nil
+ return &m.pkg, nil
}
diff --git a/grype/matcher/java/matcher_test.go b/grype/matcher/java/matcher_test.go
index b3dcdf64371..83e2a4b4eb3 100644
--- a/grype/matcher/java/matcher_test.go
+++ b/grype/matcher/java/matcher_test.go
@@ -14,54 +14,278 @@ import (
)
func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) {
- p := pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "org.springframework.spring-webmvc",
- Version: "5.1.5.RELEASE",
- Language: syftPkg.Java,
- Type: syftPkg.JavaPkg,
- MetadataType: pkg.JavaMetadataType,
- Metadata: pkg.JavaMetadata{
- ArchiveDigests: []pkg.Digest{
+ newMatcher := func(searcher MavenSearcher) *Matcher {
+ return &Matcher{
+ cfg: MatcherConfig{
+ ExternalSearchConfig: ExternalSearchConfig{
+ SearchMavenUpstream: true,
+ },
+ },
+ MavenSearcher: searcher,
+ }
+ }
+ store := newMockProvider()
+
+ // 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",
+ },
+ },
+ },
},
},
},
- }
- matcher := Matcher{
- cfg: MatcherConfig{
- ExternalSearchConfig: ExternalSearchConfig{
- SearchMavenUpstream: true,
+ {
+ 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: "",
+ },
+ },
+ },
+ },
},
- UseCPEs: false,
},
- MavenSearcher: newMockSearcher(p),
}
- store := newMockProvider()
- actual, _ := matcher.matchUpstreamMavenPackages(store, nil, p)
- assert.Len(t, actual, 2, "unexpected matches count")
+ 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, _ := matcher.matchUpstreamMavenPackages(store, p.packages[0])
+
+ assert.Len(t, actual, 2, "unexpected matches count")
- foundCVEs := stringutil.NewStringSet()
- for _, v := range actual {
- foundCVEs.Add(v.Vulnerability.ID)
+ 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")
+ 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)
+ }
+
+ })
}
- assert.Equal(t, p.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)
+ t.Run("handles maven rate limiting", func(t *testing.T) {
+ 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.packages[0])
+
+ 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,
}
}
- if t.Failed() {
- t.Logf("discovered CVES: %+v", foundCVEs)
+
+ // 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/java/maven_search.go b/grype/matcher/java/maven_search.go
index b7c37d7bf05..2e6dfd34b68 100644
--- a/grype/matcher/java/maven_search.go
+++ b/grype/matcher/java/maven_search.go
@@ -1,11 +1,15 @@
package java
import (
+ "context"
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
+ "time"
+
+ "golang.org/x/time/rate"
"github.com/anchore/grype/grype/pkg"
syftPkg "github.com/anchore/syft/syft/pkg"
@@ -14,13 +18,24 @@ import (
// MavenSearcher is the interface that wraps the GetMavenPackageBySha method.
type MavenSearcher interface {
// GetMavenPackageBySha provides an interface for building a package from maven data based on a sha1 digest
- GetMavenPackageBySha(string) (*pkg.Package, error)
+ GetMavenPackageBySha(context.Context, string) (*pkg.Package, error)
}
// mavenSearch implements the MavenSearcher interface
type mavenSearch struct {
- client *http.Client
- baseURL string
+ client *http.Client
+ baseURL string
+ rateLimiter *rate.Limiter
+}
+
+// newMavenSearch creates a new mavenSearch instance with rate limiting
+// rate is specified as 1 request per 300ms
+func newMavenSearch(client *http.Client, baseURL string, rateLimit time.Duration) *mavenSearch {
+ return &mavenSearch{
+ client: client,
+ baseURL: baseURL,
+ rateLimiter: rate.NewLimiter(rate.Every(rateLimit), 1),
+ }
}
type mavenAPIResponse struct {
@@ -37,8 +52,27 @@ type mavenAPIResponse struct {
} `json:"response"`
}
-func (ms *mavenSearch) GetMavenPackageBySha(sha1 string) (*pkg.Package, error) {
- req, err := http.NewRequest(http.MethodGet, ms.baseURL, nil)
+func (ms *mavenSearch) GetMavenPackageBySha(ctx context.Context, sha1 string) (*pkg.Package, error) {
+ if sha1 == "" {
+ return nil, errors.New("empty sha1 digest")
+ }
+ if ms.baseURL == "" {
+ return nil, errors.New("empty maven search URL")
+ }
+ if ms.rateLimiter == nil {
+ return nil, errors.New("rate limiter not initialized")
+ }
+ if ms.client == nil {
+ return nil, errors.New("HTTP client not initialized")
+ }
+
+ // Wait for rate limiter
+ err := ms.rateLimiter.Wait(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("rate limiter error: %w", err)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, ms.baseURL, nil)
if err != nil {
return nil, fmt.Errorf("unable to initialize HTTP client: %w", err)
}
diff --git a/grype/matcher/java/maven_test.go b/grype/matcher/java/maven_test.go
new file mode 100644
index 00000000000..3427cd8d36d
--- /dev/null
+++ b/grype/matcher/java/maven_test.go
@@ -0,0 +1,100 @@
+package java
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "golang.org/x/time/rate"
+)
+
+func TestNewMavenSearchRateLimiter(t *testing.T) {
+ // Create a test server
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // We don't need to respond with anything for this test
+ }))
+ defer ts.Close()
+
+ t.Run("custom rate limit initialization", func(t *testing.T) {
+ customDuration := 500 * time.Millisecond
+ ms := newMavenSearch(http.DefaultClient, ts.URL, customDuration)
+
+ expectedRate := rate.Every(customDuration)
+ if ms.rateLimiter.Limit() != expectedRate {
+ t.Errorf("unexpected rate limit: got %v, want %v", ms.rateLimiter.Limit(), rate.Limit(expectedRate))
+ }
+ })
+
+ t.Run("default rate limit initialization", func(t *testing.T) {
+ defaultDuration := 300 * time.Millisecond
+ ms := newMavenSearch(http.DefaultClient, ts.URL, defaultDuration)
+
+ expectedRate := rate.Every(defaultDuration)
+ if ms.rateLimiter.Limit() != expectedRate {
+ t.Errorf("unexpected rate limit: got %v, want %v", ms.rateLimiter.Limit(), rate.Limit(expectedRate))
+ }
+ })
+
+ t.Run("rate limiter behavior", func(t *testing.T) {
+ ms := newMavenSearch(http.DefaultClient, ts.URL, 200*time.Millisecond)
+ ctx := context.Background()
+
+ // First request should proceed immediately
+ start := time.Now()
+ err := ms.rateLimiter.Wait(ctx)
+ if err != nil {
+ t.Errorf("unexpected error on first wait: %v", err)
+ }
+ if elapsed := time.Since(start); elapsed > 50*time.Millisecond {
+ t.Errorf("first request took too long: %v", elapsed)
+ }
+
+ // Second request should be delayed
+ start = time.Now()
+ err = ms.rateLimiter.Wait(ctx)
+ if err != nil {
+ t.Errorf("unexpected error on second wait: %v", err)
+ }
+ if elapsed := time.Since(start); elapsed < 150*time.Millisecond {
+ t.Errorf("rate limiting not enforced, second request took: %v", elapsed)
+ }
+ })
+
+ t.Run("config integration", func(t *testing.T) {
+ testCases := []struct {
+ name string
+ rateLimit time.Duration
+ want rate.Limit
+ }{
+ {
+ name: "with default rate limit",
+ rateLimit: 300 * time.Millisecond,
+ want: rate.Every(300 * time.Millisecond),
+ },
+ {
+ name: "with custom rate limit",
+ rateLimit: 500 * time.Millisecond,
+ want: rate.Every(500 * time.Millisecond),
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ms := newMavenSearch(http.DefaultClient, ts.URL, tc.rateLimit)
+ if ms.rateLimiter.Limit() != tc.want {
+ t.Errorf("rate limit = %v, want %v", ms.rateLimiter.Limit(), tc.want)
+ }
+ })
+ }
+ })
+}
+
+func withinDelta(got, want, delta time.Duration) bool {
+ diff := got - want
+ if diff < 0 {
+ diff = -diff
+ }
+ return diff <= delta
+}
diff --git a/grype/matcher/javascript/matcher.go b/grype/matcher/javascript/matcher.go
index 9f8d596dc69..c0057ccbdc9 100644
--- a/grype/matcher/javascript/matcher.go
+++ b/grype/matcher/javascript/matcher.go
@@ -1,10 +1,9 @@
package javascript
import (
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType {
return match.JavascriptMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
- criteria := search.CommonCriteria
- if m.cfg.UseCPEs {
- criteria = append(criteria, search.ByCPE)
- }
- return search.ByCriteria(store, d, p, m.Type(), criteria...)
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
+ return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)
}
diff --git a/grype/matcher/matcher.go b/grype/matcher/matcher.go
deleted file mode 100644
index a1af786860b..00000000000
--- a/grype/matcher/matcher.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package matcher
-
-import (
- "github.com/anchore/grype/grype/distro"
- "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"
-)
-
-type Matcher interface {
- PackageTypes() []syftPkg.Type
- Type() match.MatcherType
- Match(vulnerability.Provider, *distro.Distro, pkg.Package) ([]match.Match, error)
-}
diff --git a/grype/matcher/matchers.go b/grype/matcher/matchers.go
index 7181ac330b8..978f31ac5e4 100644
--- a/grype/matcher/matchers.go
+++ b/grype/matcher/matchers.go
@@ -1,13 +1,6 @@
package matcher
import (
- "github.com/wagoodman/go-partybus"
- "github.com/wagoodman/go-progress"
-
- grypeDb "github.com/anchore/grype/grype/db/v5"
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/event"
- "github.com/anchore/grype/grype/event/monitor"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher/apk"
"github.com/anchore/grype/grype/matcher/dotnet"
@@ -20,58 +13,10 @@ import (
"github.com/anchore/grype/grype/matcher/python"
"github.com/anchore/grype/grype/matcher/rpm"
"github.com/anchore/grype/grype/matcher/ruby"
+ "github.com/anchore/grype/grype/matcher/rust"
"github.com/anchore/grype/grype/matcher/stock"
- "github.com/anchore/grype/grype/pkg"
- "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"
)
-type monitorWriter struct {
- PackagesProcessed *progress.Manual
- VulnerabilitiesDiscovered *progress.Manual
- Fixed *progress.Manual
- BySeverity map[vulnerability.Severity]*progress.Manual
-}
-
-func newMonitor() (monitorWriter, monitor.Matching) {
- manualBySev := make(map[vulnerability.Severity]*progress.Manual)
- for _, severity := range vulnerability.AllSeverities() {
- manualBySev[severity] = progress.NewManual(-1)
- }
- manualBySev[vulnerability.UnknownSeverity] = progress.NewManual(-1)
-
- m := monitorWriter{
- PackagesProcessed: progress.NewManual(-1),
- VulnerabilitiesDiscovered: progress.NewManual(-1),
- Fixed: progress.NewManual(-1),
- BySeverity: manualBySev,
- }
-
- monitorableBySev := make(map[vulnerability.Severity]progress.Monitorable)
- for sev, manual := range manualBySev {
- monitorableBySev[sev] = manual
- }
-
- return m, monitor.Matching{
- PackagesProcessed: m.PackagesProcessed,
- VulnerabilitiesDiscovered: m.VulnerabilitiesDiscovered,
- Fixed: m.Fixed,
- BySeverity: monitorableBySev,
- }
-}
-
-func (m *monitorWriter) SetCompleted() {
- m.PackagesProcessed.SetCompleted()
- m.VulnerabilitiesDiscovered.SetCompleted()
- m.Fixed.SetCompleted()
- for _, v := range m.BySeverity {
- v.SetCompleted()
- }
-}
-
// Config contains values used by individual matcher structs for advanced configuration
type Config struct {
Java java.MatcherConfig
@@ -80,11 +25,12 @@ type Config struct {
Dotnet dotnet.MatcherConfig
Javascript javascript.MatcherConfig
Golang golang.MatcherConfig
+ Rust rust.MatcherConfig
Stock stock.MatcherConfig
}
-func NewDefaultMatchers(mc Config) []Matcher {
- return []Matcher{
+func NewDefaultMatchers(mc Config) []match.Matcher {
+ return []match.Matcher{
&dpkg.Matcher{},
ruby.NewRubyMatcher(mc.Ruby),
python.NewPythonMatcher(mc.Python),
@@ -96,168 +42,7 @@ func NewDefaultMatchers(mc Config) []Matcher {
golang.NewGolangMatcher(mc.Golang),
&msrc.Matcher{},
&portage.Matcher{},
+ rust.NewRustMatcher(mc.Rust),
stock.NewStockMatcher(mc.Stock),
}
}
-
-func trackMatcher() *monitorWriter {
- writer, reader := newMonitor()
-
- bus.Publish(partybus.Event{
- Type: event.VulnerabilityScanningStarted,
- Value: reader,
- })
-
- return &writer
-}
-
-func newMatcherIndex(matchers []Matcher) (map[syftPkg.Type][]Matcher, Matcher) {
- matcherIndex := make(map[syftPkg.Type][]Matcher)
- var defaultMatcher Matcher
- for _, m := range matchers {
- if m.Type() == match.StockMatcher {
- defaultMatcher = m
- continue
- }
- for _, t := range m.PackageTypes() {
- if _, ok := matcherIndex[t]; !ok {
- matcherIndex[t] = make([]Matcher, 0)
- }
-
- matcherIndex[t] = append(matcherIndex[t], m)
- log.Debugf("adding matcher: %+v", t)
- }
- }
-
- return matcherIndex, defaultMatcher
-}
-
-func FindMatches(store interface {
- vulnerability.Provider
- vulnerability.MetadataProvider
- match.ExclusionProvider
-}, release *linux.Release, matchers []Matcher, packages []pkg.Package) match.Matches {
- var err error
- res := match.NewMatches()
- matcherIndex, defaultMatcher := newMatcherIndex(matchers)
-
- var ignored []match.IgnoredMatch
-
- var d *distro.Distro
- if release != nil {
- d, err = distro.NewFromRelease(*release)
- if err != nil {
- log.Warnf("unable to determine linux distribution: %+v", err)
- }
- if d != nil && d.Disabled() {
- log.Warnf("unsupported linux distribution: %s", d.Name())
- return match.Matches{}
- }
- }
-
- progressMonitor := trackMatcher()
-
- if defaultMatcher == nil {
- defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true})
- }
- for _, p := range packages {
- progressMonitor.PackagesProcessed.Increment()
- log.Debugf("searching for vulnerability matches for pkg=%s", p)
-
- matchAgainst, ok := matcherIndex[p.Type]
- if !ok {
- matchAgainst = []Matcher{defaultMatcher}
- }
- for _, m := range matchAgainst {
- matches, err := m.Match(store, d, p)
- if err != nil {
- log.Warnf("matcher failed for pkg=%s: %+v", p, err)
- } else {
- // Filter out matches based on records in the database exclusion table and hard-coded rules
- filtered, ignores := match.ApplyExplicitIgnoreRules(store, match.NewMatches(matches...))
- ignored = append(ignored, ignores...)
- matches := filtered.Sorted()
- logMatches(p, matches)
- res.Add(matches...)
- progressMonitor.VulnerabilitiesDiscovered.Add(int64(len(matches)))
- updateVulnerabilityList(progressMonitor, matches, store)
- }
- }
- }
-
- progressMonitor.SetCompleted()
-
- logListSummary(progressMonitor)
-
- logIgnoredMatches(ignored)
-
- return res
-}
-
-func logListSummary(vl *monitorWriter) {
- log.Infof("found %d vulnerabilities for %d packages", vl.VulnerabilitiesDiscovered.Current(), vl.PackagesProcessed.Current())
- log.Debugf(" ├── fixed: %d", vl.Fixed.Current())
- log.Debugf(" └── matched: %d", vl.VulnerabilitiesDiscovered.Current())
-
- var unknownCount int64
- if count, ok := vl.BySeverity[vulnerability.UnknownSeverity]; ok {
- unknownCount = count.Current()
- }
- log.Debugf(" ├── %s: %d", vulnerability.UnknownSeverity.String(), unknownCount)
-
- allSeverities := vulnerability.AllSeverities()
- for idx, sev := range allSeverities {
- branch := "├"
- if idx == len(allSeverities)-1 {
- branch = "└"
- }
- log.Debugf(" %s── %s: %d", branch, sev.String(), vl.BySeverity[sev].Current())
- }
-}
-
-func logIgnoredMatches(ignored []match.IgnoredMatch) {
- if len(ignored) > 0 {
- log.Debugf("Removed %d explicit vulnerability matches:", len(ignored))
- for idx, i := range ignored {
- branch := "├──"
- if idx == len(ignored)-1 {
- branch = "└──"
- }
- log.Debugf(" %s %s : %s", branch, i.Match.Vulnerability.ID, i.Package.PURL)
- }
- }
-}
-
-func updateVulnerabilityList(list *monitorWriter, matches []match.Match, metadataProvider vulnerability.MetadataProvider) {
- for _, m := range matches {
- metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
- if err != nil || metadata == nil {
- list.BySeverity[vulnerability.UnknownSeverity].Increment()
- continue
- }
-
- sevManualProgress, ok := list.BySeverity[vulnerability.ParseSeverity(metadata.Severity)]
- if !ok {
- list.BySeverity[vulnerability.UnknownSeverity].Increment()
- continue
- }
- sevManualProgress.Increment()
-
- if m.Vulnerability.Fix.State == grypeDb.FixedState {
- list.Fixed.Increment()
- }
- }
-}
-
-func logMatches(p pkg.Package, matches []match.Match) {
- if len(matches) > 0 {
- log.Debugf("found %d vulnerabilities for pkg=%s", len(matches), p)
- for idx, m := range matches {
- var branch = "├──"
- if idx == len(matches)-1 {
- branch = "└──"
- }
- log.Debugf(" %s %s", branch, m.Summary())
- }
- }
-}
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.go b/grype/matcher/msrc/matcher.go
index 1fd95baa570..edb038b2619 100644
--- a/grype/matcher/msrc/matcher.go
+++ b/grype/matcher/msrc/matcher.go
@@ -1,10 +1,9 @@
package msrc
import (
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -23,9 +22,9 @@ func (m *Matcher) Type() match.MatcherType {
return match.MsrcMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
// find KB matches for the MSFT version given in the package and version.
// The "distro" holds the information about the Windows version, and its
// patch (KB)
- return search.ByCriteria(store, d, p, m.Type(), search.ByDistro)
+ return internal.MatchPackageByDistro(store, p, m.Type())
}
diff --git a/grype/matcher/msrc/matcher_test.go b/grype/matcher/msrc/matcher_test.go
index 095f6097bf9..f42e5a0a1d2 100644
--- a/grype/matcher/msrc/matcher_test.go
+++ b/grype/matcher/msrc/matcher_test.go
@@ -5,83 +5,52 @@ import (
"testing"
"github.com/google/uuid"
- "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/anchore/grype/grype/db"
- grypeDB "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
syftPkg "github.com/anchore/syft/syft/pkg"
)
-type mockStore struct {
- backend map[string]map[string][]grypeDB.Vulnerability
-}
-
-func (s *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
- //TODO implement me
- panic("implement me")
-}
-
-func (s *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
- namespaceMap := s.backend[namespace]
- if namespaceMap == nil {
- return nil, nil
- }
- return namespaceMap[name], nil
-}
-
-func (s *mockStore) GetAllVulnerabilities() (*[]grypeDB.Vulnerability, error) {
- return nil, nil
-}
-
-func (s *mockStore) GetVulnerabilityNamespaces() ([]string, error) {
- keys := make([]string, 0, len(s.backend))
- for k := range s.backend {
- keys = append(keys, k)
- }
-
- return keys, nil
-}
-
func TestMatches(t *testing.T) {
d, err := distro.New(distro.Windows, "10816", "Windows Server 2016")
- assert.NoError(t, err)
+ require.NoError(t, err)
- store := mockStore{
- backend: map[string]map[string][]grypeDB.Vulnerability{
+ // 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.Version)
- // 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
- fmt.Sprintf("msrc:distro:windows:%s", d.RawVersion): {
- d.RawVersion: []grypeDB.Vulnerability{
- {
- ID: "CVE-2016-3333",
- VersionConstraint: "3200970 || 878787 || base",
- VersionFormat: "kb",
- },
- {
- // Does not match, version constraints do not apply
- ID: "CVE-2020-made-up",
- VersionConstraint: "778786 || 878787 || base",
- VersionFormat: "kb",
- },
- },
- // Does not match the product ID
- "something-else": []grypeDB.Vulnerability{
- {
- ID: "CVE-2020-also-made-up",
- VersionConstraint: "3200970 || 878787 || base",
- VersionFormat: "kb",
- },
- },
+ vp := mock.VulnerabilityProvider([]vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2016-3333",
+ Namespace: msrcNamespace,
},
+ PackageName: d.Version,
+ Constraint: version.MustGetConstraint("3200970 || 878787 || base", version.KBFormat),
},
- }
-
- provider, err := db.NewVulnerabilityProvider(&store)
- require.NoError(t, err)
+ {
+ Reference: vulnerability.Reference{
+ // Does not match, version constraints do not apply
+ ID: "CVE-2020-made-up",
+ Namespace: msrcNamespace,
+ },
+ PackageName: d.Version,
+ Constraint: version.MustGetConstraint("778786 || 878787 || base", version.KBFormat),
+ },
+ // Does not match the product ID
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2020-also-made-up",
+ Namespace: msrcNamespace,
+ },
+ PackageName: "something-else",
+ Constraint: version.MustGetConstraint("3200970 || 878787 || base", version.KBFormat),
+ },
+ }...)
tests := []struct {
name string
@@ -92,9 +61,10 @@ 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,
},
expectedVulnIDs: []string{
"CVE-2016-3333",
@@ -104,9 +74,10 @@ 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,
},
expectedVulnIDs: []string{
"CVE-2016-3333",
@@ -117,10 +88,11 @@ 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,
+ Distro: d,
},
expectedVulnIDs: []string{
"CVE-2016-3333",
@@ -132,13 +104,13 @@ func TestMatches(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
m := Matcher{}
- matches, err := m.Match(provider, d, test.pkg)
- assert.NoError(t, err)
+ matches, _, err := m.Match(vp, test.pkg)
+ require.NoError(t, err)
var actualVulnIDs []string
for _, a := range matches {
actualVulnIDs = append(actualVulnIDs, a.Vulnerability.ID)
}
- assert.ElementsMatch(t, test.expectedVulnIDs, actualVulnIDs)
+ require.ElementsMatch(t, test.expectedVulnIDs, actualVulnIDs)
})
}
diff --git a/grype/matcher/portage/matcher.go b/grype/matcher/portage/matcher.go
index ddda50700e4..2126cd337c7 100644
--- a/grype/matcher/portage/matcher.go
+++ b/grype/matcher/portage/matcher.go
@@ -1,12 +1,9 @@
package portage
import (
- "fmt"
-
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -22,11 +19,6 @@ func (m *Matcher) Type() match.MatcherType {
return match.PortageMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
- matches, err := search.ByPackageDistro(store, d, p, m.Type())
- if err != nil {
- return nil, fmt.Errorf("failed to find vulnerabilities: %w", err)
- }
-
- return matches, nil
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
+ return internal.MatchPackageByDistro(store, p, m.Type())
}
diff --git a/grype/matcher/portage/matcher_mocks_test.go b/grype/matcher/portage/matcher_mocks_test.go
index e5919b7b7ac..612729dc107 100644
--- a/grype/matcher/portage/matcher_mocks_test.go
+++ b/grype/matcher/portage/matcher_mocks_test.go
@@ -1,57 +1,23 @@
package portage
import (
- "strings"
-
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/pkg"
"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"
+ "github.com/anchore/grype/grype/vulnerability/mock"
)
-type mockProvider struct {
- data map[string]map[string][]vulnerability.Vulnerability
-}
-
-func (pr *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
- //TODO implement me
- panic("implement me")
-}
-
-func newMockProvider() *mockProvider {
- pr := mockProvider{
- data: make(map[string]map[string][]vulnerability.Vulnerability),
- }
- pr.stub()
- return &pr
-}
-
-func (pr *mockProvider) stub() {
- pr.data["gentoo:"] = map[string][]vulnerability.Vulnerability{
+func newMockProvider() vulnerability.Provider {
+ return mock.VulnerabilityProvider([]vulnerability.Vulnerability{
// direct...
- "app-misc/neutron": {
- {
- Constraint: version.MustGetConstraint("< 2014.1.3", version.PortageFormat),
- ID: "CVE-2014-fake-1",
- },
- {
- Constraint: version.MustGetConstraint("< 2014.1.4", version.PortageFormat),
- ID: "CVE-2014-fake-2",
- },
+ {
+ PackageName: "app-misc/neutron",
+ Constraint: version.MustGetConstraint("< 2014.1.3", version.PortageFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2014-fake-1", Namespace: "secdb:distro:gentoo:"},
},
- }
-}
-
-func (pr *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- return pr.data[strings.ToLower(d.Type.String())+":"+d.FullVersion()][p.Name], nil
-}
-
-func (pr *mockProvider) GetByCPE(request cpe.CPE) (v []vulnerability.Vulnerability, err error) {
- return v, err
-}
-
-func (pr *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) (v []vulnerability.Vulnerability, err error) {
- return v, err
+ {
+ PackageName: "app-misc/neutron",
+ Constraint: version.MustGetConstraint("< 2014.1.4", version.PortageFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "secdb:distro:gentoo:"},
+ },
+ }...)
}
diff --git a/grype/matcher/portage/matcher_test.go b/grype/matcher/portage/matcher_test.go
index 2c3c769a59f..9814d9a526b 100644
--- a/grype/matcher/portage/matcher_test.go
+++ b/grype/matcher/portage/matcher_test.go
@@ -15,20 +15,22 @@ import (
func TestMatcherPortage_Match(t *testing.T) {
matcher := Matcher{}
+
+ d, err := distro.New(distro.Gentoo, "", "")
+ if err != nil {
+ t.Fatal("could not create distro: ", err)
+ }
+
p := pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "app-misc/neutron",
Version: "2014.1.3",
Type: syftPkg.PortagePkg,
- }
-
- d, err := distro.New(distro.Gentoo, "", "")
- if err != nil {
- t.Fatal("could not create distro: ", err)
+ Distro: d,
}
store := newMockProvider()
- actual, err := matcher.Match(store, d, p)
+ actual, _, err := matcher.Match(store, p)
assert.NoError(t, err, "unexpected err from Match", err)
assert.Len(t, actual, 1, "unexpected indirect matches count")
diff --git a/grype/matcher/python/matcher.go b/grype/matcher/python/matcher.go
index 64057636216..78916a52e3b 100644
--- a/grype/matcher/python/matcher.go
+++ b/grype/matcher/python/matcher.go
@@ -1,10 +1,9 @@
package python
import (
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType {
return match.PythonMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
- criteria := search.CommonCriteria
- if m.cfg.UseCPEs {
- criteria = append(criteria, search.ByCPE)
- }
- return search.ByCriteria(store, d, p, m.Type(), criteria...)
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
+ return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)
}
diff --git a/grype/matcher/rpm/matcher.go b/grype/matcher/rpm/matcher.go
index 9840f0c1f18..59971faaa9e 100644
--- a/grype/matcher/rpm/matcher.go
+++ b/grype/matcher/rpm/matcher.go
@@ -4,10 +4,9 @@ import (
"fmt"
"strings"
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -24,7 +23,7 @@ func (m *Matcher) Type() match.MatcherType {
}
//nolint:funlen
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
matches := make([]match.Match, 0)
// let's match with a synthetic package that doesn't exist. We will create a new
@@ -72,9 +71,9 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa
// really assume an epoch of 4 on the other side). This could still lead to
// problems since an epoch delimits potentially non-comparable version lineages.
- sourceMatches, err := m.matchUpstreamPackages(store, d, p)
+ sourceMatches, err := m.matchUpstreamPackages(store, p)
if err != nil {
- return nil, fmt.Errorf("failed to match by source indirection: %w", err)
+ return nil, nil, fmt.Errorf("failed to match by source indirection: %w", err)
}
matches = append(matches, sourceMatches...)
@@ -94,21 +93,21 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa
// case). To do this we fill in missing epoch values in the package versions with
// an explicit 0.
- exactMatches, err := m.matchPackage(store, d, p)
+ exactMatches, err := m.matchPackage(store, p)
if err != nil {
- return nil, fmt.Errorf("failed to match by exact package name: %w", err)
+ return nil, nil, fmt.Errorf("failed to match by exact package name: %w", err)
}
matches = append(matches, exactMatches...)
- return matches, nil
+ return matches, nil, nil
}
-func (m *Matcher) matchUpstreamPackages(store vulnerability.ProviderByDistro, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) matchUpstreamPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) {
var matches []match.Match
for _, indirectPackage := range pkg.UpstreamPackages(p) {
- indirectMatches, err := search.ByPackageDistro(store, d, indirectPackage, m.Type())
+ indirectMatches, _, err := internal.MatchPackageByDistro(store, indirectPackage, m.Type())
if err != nil {
return nil, fmt.Errorf("failed to find vulnerabilities for rpm upstream source package: %w", err)
}
@@ -122,13 +121,13 @@ func (m *Matcher) matchUpstreamPackages(store vulnerability.ProviderByDistro, d
return matches, nil
}
-func (m *Matcher) matchPackage(store vulnerability.ProviderByDistro, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
+func (m *Matcher) matchPackage(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) {
// we want to ensure that the version ALWAYS has an epoch specified...
originalPkg := p
- p.Version = addZeroEpicIfApplicable(p.Version)
+ addEpochIfApplicable(&p)
- matches, err := search.ByPackageDistro(store, d, p, m.Type())
+ matches, _, err := internal.MatchPackageByDistro(store, p, m.Type())
if err != nil {
return nil, fmt.Errorf("failed to find vulnerabilities by dpkg source indirection: %w", err)
}
@@ -141,9 +140,18 @@ func (m *Matcher) matchPackage(store vulnerability.ProviderByDistro, d *distro.D
return matches, nil
}
-func addZeroEpicIfApplicable(version string) string {
- if strings.Contains(version, ":") {
- return version
+func addEpochIfApplicable(p *pkg.Package) {
+ meta, ok := p.Metadata.(pkg.RpmMetadata)
+ version := p.Version
+ switch {
+ case strings.Contains(version, ":"):
+ // we already have an epoch embedded in the version string
+ return
+ case ok && meta.Epoch != nil:
+ // we have an explicit epoch in the metadata
+ p.Version = fmt.Sprintf("%d:%s", *meta.Epoch, version)
+ default:
+ // no epoch was found, so we will add one
+ p.Version = "0:" + version
}
- return "0:" + version
}
diff --git a/grype/matcher/rpm/matcher_mocks_test.go b/grype/matcher/rpm/matcher_mocks_test.go
index 21889717e5c..781e580412a 100644
--- a/grype/matcher/rpm/matcher_mocks_test.go
+++ b/grype/matcher/rpm/matcher_mocks_test.go
@@ -1,145 +1,112 @@
package rpm
import (
- "strings"
-
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/pkg/qualifier"
"github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity"
"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"
+ "github.com/anchore/grype/grype/vulnerability/mock"
)
-type mockProvider struct {
- data map[string]map[string][]vulnerability.Vulnerability
-}
-
-func (pr *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
- //TODO implement me
- panic("implement me")
-}
-
-func newMockProvider(packageName, indirectName string, withEpoch bool, withPackageQualifiers bool) *mockProvider {
- pr := mockProvider{
- data: make(map[string]map[string][]vulnerability.Vulnerability),
- }
+func newMockProvider(packageName, indirectName string, withEpoch bool, withPackageQualifiers bool) vulnerability.Provider {
if withEpoch {
- pr.stubWithEpoch(packageName, indirectName)
+ return mock.VulnerabilityProvider(vulnerabilitiesWithEpoch(packageName, indirectName)...)
} else if withPackageQualifiers {
- pr.stubWithPackageQualifiers(packageName)
- } else {
- pr.stub(packageName, indirectName)
+ return mock.VulnerabilityProvider(vulnerabilitiesWithPackageQualifiers(packageName)...)
}
-
- return &pr
+ return mock.VulnerabilityProvider(vulnerabilitiesDefaults(packageName, indirectName)...)
}
-func (pr *mockProvider) stub(packageName, indirectName string) {
- pr.data["rhel:8"] = map[string][]vulnerability.Vulnerability{
+const namespace = "secdb:distro:centos:8"
+
+func vulnerabilitiesDefaults(packageName, indirectName string) []vulnerability.Vulnerability {
+ return []vulnerability.Vulnerability{
// direct...
- packageName: {
- {
- Constraint: version.MustGetConstraint("<= 7.1.3-6", version.RpmFormat),
- ID: "CVE-2014-fake-1",
- },
+ {
+ PackageName: packageName,
+ Constraint: version.MustGetConstraint("<= 7.1.3-6", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2014-fake-1", Namespace: namespace},
},
// indirect...
- indirectName: {
- // expected...
- {
- Constraint: version.MustGetConstraint("< 7.1.4-5", version.RpmFormat),
- ID: "CVE-2014-fake-2",
- },
- {
- Constraint: version.MustGetConstraint("< 8.0.2-0", version.RpmFormat),
- ID: "CVE-2013-fake-3",
- },
- // unexpected...
- {
- Constraint: version.MustGetConstraint("< 7.0.4-1", version.RpmFormat),
- ID: "CVE-2013-fake-BAD",
- },
+ // expected...
+ {
+ PackageName: indirectName,
+ Constraint: version.MustGetConstraint("< 7.1.4-5", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: namespace},
+ },
+ {
+ PackageName: indirectName,
+ Constraint: version.MustGetConstraint("< 8.0.2-0", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: namespace},
+ },
+ // unexpected...
+ {
+ PackageName: indirectName,
+ Constraint: version.MustGetConstraint("< 7.0.4-1", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: namespace},
},
}
}
-func (pr *mockProvider) stubWithEpoch(packageName, indirectName string) {
- pr.data["rhel:8"] = map[string][]vulnerability.Vulnerability{
+func vulnerabilitiesWithEpoch(packageName, indirectName string) []vulnerability.Vulnerability {
+ return []vulnerability.Vulnerability{
// direct...
- packageName: {
- {
- Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
- ID: "CVE-2021-1",
- },
- {
- Constraint: version.MustGetConstraint("<= 0:2.28-419.el8.", version.RpmFormat),
- ID: "CVE-2021-2",
- },
+ {
+ PackageName: packageName,
+ Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2021-1", Namespace: namespace},
+ },
+ {
+ PackageName: packageName,
+ Constraint: version.MustGetConstraint("<= 0:2.28-419.el8.", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2021-2", Namespace: namespace},
},
// indirect...
- indirectName: {
- {
- Constraint: version.MustGetConstraint("< 5.28.3-420.el8", version.RpmFormat),
- ID: "CVE-2021-3",
- },
- // unexpected...
- {
- Constraint: version.MustGetConstraint("< 4:5.26.3-419.el8", version.RpmFormat),
- ID: "CVE-2021-4",
- },
+ {
+ PackageName: indirectName,
+ Constraint: version.MustGetConstraint("< 5.28.3-420.el8", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2021-3", Namespace: namespace},
+ },
+ // unexpected...
+ {
+ PackageName: indirectName,
+ Constraint: version.MustGetConstraint("< 4:5.26.3-419.el8", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2021-4", Namespace: namespace},
},
}
}
-func (pr *mockProvider) stubWithPackageQualifiers(packageName string) {
- pr.data["rhel:8"] = map[string][]vulnerability.Vulnerability{
+func vulnerabilitiesWithPackageQualifiers(packageName string) []vulnerability.Vulnerability {
+ return []vulnerability.Vulnerability{
// direct...
- packageName: {
- {
- Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
- ID: "CVE-2021-1",
- PackageQualifiers: []qualifier.Qualifier{
- rpmmodularity.New("containertools:3"),
- },
+ {
+ PackageName: packageName,
+ Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2021-1", Namespace: namespace},
+ PackageQualifiers: []qualifier.Qualifier{
+ rpmmodularity.New("containertools:3"),
},
- {
- Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
- ID: "CVE-2021-2",
- PackageQualifiers: []qualifier.Qualifier{
- rpmmodularity.New(""),
- },
- },
- {
- Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
- ID: "CVE-2021-3",
+ },
+ {
+ PackageName: packageName,
+ Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2021-2", Namespace: namespace},
+ PackageQualifiers: []qualifier.Qualifier{
+ rpmmodularity.New(""),
},
- {
- Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
- ID: "CVE-2021-4",
- PackageQualifiers: []qualifier.Qualifier{
- rpmmodularity.New("containertools:4"),
- },
+ },
+ {
+ PackageName: packageName,
+ Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2021-3", Namespace: namespace},
+ },
+ {
+ PackageName: packageName,
+ Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2021-4", Namespace: namespace},
+ PackageQualifiers: []qualifier.Qualifier{
+ rpmmodularity.New("containertools:4"),
},
},
}
}
-
-func (pr *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- var ty = strings.ToLower(d.Type.String())
- if d.Type == distro.CentOS || d.Type == distro.RedHat || d.Type == distro.RockyLinux || d.Type == distro.AlmaLinux {
- ty = "rhel"
- }
-
- return pr.data[ty+":"+d.FullVersion()][p.Name], nil
-}
-
-func (pr *mockProvider) GetByCPE(request cpe.CPE) (v []vulnerability.Vulnerability, err error) {
- return v, err
-}
-
-func (pr *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) (v []vulnerability.Vulnerability, err error) {
- return v, err
-}
diff --git a/grype/matcher/rpm/matcher_test.go b/grype/matcher/rpm/matcher_test.go
index d7959b0aca2..568ae6f8af6 100644
--- a/grype/matcher/rpm/matcher_test.go
+++ b/grype/matcher/rpm/matcher_test.go
@@ -248,13 +248,12 @@ func TestMatcherRpm(t *testing.T) {
{
name: "package with modularity label 1",
p: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "maniac",
- Version: "0.1",
- Type: syftPkg.RpmPkg,
- MetadataType: pkg.RpmMetadataType,
+ ID: pkg.ID(uuid.NewString()),
+ Name: "maniac",
+ Version: "0.1",
+ Type: syftPkg.RpmPkg,
Metadata: pkg.RpmMetadata{
- ModularityLabel: "containertools:3:1234:5678",
+ ModularityLabel: strRef("containertools:3:1234:5678"),
},
},
setup: func() (vulnerability.Provider, *distro.Distro, Matcher) {
@@ -276,13 +275,12 @@ func TestMatcherRpm(t *testing.T) {
{
name: "package with modularity label 2",
p: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "maniac",
- Version: "0.1",
- Type: syftPkg.RpmPkg,
- MetadataType: pkg.RpmMetadataType,
+ ID: pkg.ID(uuid.NewString()),
+ Name: "maniac",
+ Version: "0.1",
+ Type: syftPkg.RpmPkg,
Metadata: pkg.RpmMetadata{
- ModularityLabel: "containertools:1:abc:123",
+ ModularityLabel: strRef("containertools:1:abc:123"),
},
},
setup: func() (vulnerability.Provider, *distro.Distro, Matcher) {
@@ -303,11 +301,10 @@ func TestMatcherRpm(t *testing.T) {
{
name: "package without modularity label",
p: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "maniac",
- Version: "0.1",
- Type: syftPkg.RpmPkg,
- MetadataType: pkg.RpmMetadataType,
+ ID: pkg.ID(uuid.NewString()),
+ Name: "maniac",
+ Version: "0.1",
+ Type: syftPkg.RpmPkg,
},
setup: func() (vulnerability.Provider, *distro.Distro, Matcher) {
matcher := Matcher{}
@@ -332,7 +329,10 @@ func TestMatcherRpm(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
store, d, matcher := test.setup()
- actual, err := matcher.Match(store, d, test.p)
+ if test.p.Distro == nil {
+ test.p.Distro = d
+ }
+ actual, _, err := matcher.Match(store, test.p)
if err != nil {
t.Fatal("could not find match: ", err)
}
@@ -363,24 +363,56 @@ func TestMatcherRpm(t *testing.T) {
}
}
-func Test_addZeroEpicIfApplicable(t *testing.T) {
+func Test_addEpochIfApplicable(t *testing.T) {
tests := []struct {
- version string
+ name string
+ pkg pkg.Package
expected string
}{
{
- version: "3.26.0-6.el8",
+ name: "assume 0 epoch",
+ pkg: pkg.Package{
+ Version: "3.26.0-6.el8",
+ },
expected: "0:3.26.0-6.el8",
},
{
- version: "7:3.26.0-6.el8",
+ name: "epoch already exists in version string",
+ pkg: pkg.Package{
+ Version: "7:3.26.0-6.el8",
+ },
+ expected: "7:3.26.0-6.el8",
+ },
+ {
+ name: "epoch only exists in metadata",
+ pkg: pkg.Package{
+ Version: "3.26.0-6.el8",
+ Metadata: pkg.RpmMetadata{
+ Epoch: intRef(7),
+ },
+ },
expected: "7:3.26.0-6.el8",
},
+ {
+ name: "epoch does not exist in metadata",
+ pkg: pkg.Package{
+ Version: "3.26.0-6.el8",
+ Metadata: pkg.RpmMetadata{
+ Epoch: nil,
+ },
+ },
+ expected: "0:3.26.0-6.el8",
+ },
}
for _, test := range tests {
- t.Run(test.version, func(t *testing.T) {
- actualVersion := addZeroEpicIfApplicable(test.version)
- assert.Equal(t, test.expected, actualVersion)
+ t.Run(test.name, func(t *testing.T) {
+ p := test.pkg
+ addEpochIfApplicable(&p)
+ assert.Equal(t, test.expected, p.Version)
})
}
}
+
+func strRef(s string) *string {
+ return &s
+}
diff --git a/grype/matcher/ruby/matcher.go b/grype/matcher/ruby/matcher.go
index 2a1840c1e5d..0fe094e511f 100644
--- a/grype/matcher/ruby/matcher.go
+++ b/grype/matcher/ruby/matcher.go
@@ -1,10 +1,9 @@
package ruby
import (
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType {
return match.RubyGemMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
- criteria := search.CommonCriteria
- if m.cfg.UseCPEs {
- criteria = append(criteria, search.ByCPE)
- }
- return search.ByCriteria(store, d, p, m.Type(), criteria...)
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
+ return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)
}
diff --git a/grype/matcher/rust/matcher.go b/grype/matcher/rust/matcher.go
new file mode 100644
index 00000000000..9923cb8ad0c
--- /dev/null
+++ b/grype/matcher/rust/matcher.go
@@ -0,0 +1,35 @@
+package rust
+
+import (
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/vulnerability"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+type Matcher struct {
+ cfg MatcherConfig
+}
+
+type MatcherConfig struct {
+ UseCPEs bool
+}
+
+func NewRustMatcher(cfg MatcherConfig) *Matcher {
+ return &Matcher{
+ cfg: cfg,
+ }
+}
+
+func (m *Matcher) PackageTypes() []syftPkg.Type {
+ return []syftPkg.Type{syftPkg.RustPkg}
+}
+
+func (m *Matcher) Type() match.MatcherType {
+ return match.RustMatcher
+}
+
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
+ return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)
+}
diff --git a/grype/matcher/stock/matcher.go b/grype/matcher/stock/matcher.go
index 7f30a52df9a..26dd782f2cd 100644
--- a/grype/matcher/stock/matcher.go
+++ b/grype/matcher/stock/matcher.go
@@ -1,10 +1,9 @@
package stock
import (
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)
@@ -17,7 +16,7 @@ type MatcherConfig struct {
UseCPEs bool
}
-func NewStockMatcher(cfg MatcherConfig) *Matcher {
+func NewStockMatcher(cfg MatcherConfig) match.Matcher {
return &Matcher{
cfg: cfg,
}
@@ -31,10 +30,6 @@ func (m *Matcher) Type() match.MatcherType {
return match.StockMatcher
}
-func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
- criteria := search.CommonCriteria
- if m.cfg.UseCPEs {
- criteria = append(criteria, search.ByCPE)
- }
- return search.ByCriteria(store, d, p, m.Type(), criteria...)
+func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
+ return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)
}
diff --git a/grype/matcher/stock/matcher_test.go b/grype/matcher/stock/matcher_test.go
new file mode 100644
index 00000000000..f57f959776a
--- /dev/null
+++ b/grype/matcher/stock/matcher_test.go
@@ -0,0 +1,127 @@
+package stock
+
+import (
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/scylladb/go-set/strset"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
+ "github.com/anchore/syft/syft/cpe"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func TestMatcher_JVMPackage(t *testing.T) {
+ p := pkg.Package{
+ ID: pkg.ID(uuid.NewString()),
+ Name: "java_se",
+ Version: "1.8.0_400",
+ Type: syftPkg.BinaryPkg,
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:oracle:java_se:1.8.0:update400:*:*:*:*:*:*", cpe.DeclaredSource),
+ },
+ }
+ matcher := Matcher{
+ cfg: MatcherConfig{
+ UseCPEs: true,
+ },
+ }
+ store := newMockProvider()
+ actual, _, err := matcher.Match(store, p)
+ require.NoError(t, err)
+
+ foundCVEs := strset.New()
+ for _, v := range actual {
+ foundCVEs.Add(v.Vulnerability.ID)
+
+ require.NotEmpty(t, v.Details)
+ for _, d := range v.Details {
+ assert.Equal(t, match.CPEMatch, 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")
+ }
+
+ expected := strset.New(
+ "CVE-2024-20919-real",
+ "CVE-2024-20919-bonkers-format",
+ "CVE-2024-20919-post-jep223",
+ )
+
+ for _, id := range expected.List() {
+ if !foundCVEs.Has(id) {
+ t.Errorf("missing CVE: %s", id)
+ }
+ }
+
+ extra := strset.Difference(foundCVEs, expected)
+
+ for _, id := range extra.List() {
+ t.Errorf("unexpected CVE: %s", id)
+ }
+
+ if t.Failed() {
+ t.Logf("discovered CVES: %d", foundCVEs.Size())
+ for _, id := range foundCVEs.List() {
+ t.Logf(" - %s", id)
+ }
+ }
+}
+
+func newMockProvider() vulnerability.Provider {
+ // derived from vuln data found on CVE-2024-20919
+ hit := "< 1.8.0_401 || >= 1.9-ea, < 8.0.401 || >= 9-ea, < 11.0.22 || >= 12-ea, < 17.0.10 || >= 18-ea, < 21.0.2"
+
+ cpes := []cpe.CPE{cpe.Must("cpe:2.3:a:oracle:java_se:*:*:*:*:*:*:*:*", "")}
+
+ return mock.VulnerabilityProvider([]vulnerability.Vulnerability{
+ {
+ // positive cases
+ PackageName: "java_se",
+ Constraint: version.MustGetConstraint(hit, version.JVMFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2024-20919-real", Namespace: "nvd:cpe"},
+ CPEs: cpes,
+ },
+ {
+ // positive cases
+ PackageName: "java_se",
+ Constraint: version.MustGetConstraint("< 22.22.22", version.UnknownFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2024-20919-bonkers-format", Namespace: "nvd:cpe"},
+ CPEs: cpes,
+ },
+ {
+ // negative case
+ PackageName: "java_se",
+ Constraint: version.MustGetConstraint("< 1.8.0_399 || >= 1.9-ea, < 8.0.399 || >= 9-ea", version.JVMFormat),
+ Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-update", Namespace: "nvd:cpe"},
+ CPEs: cpes,
+ },
+ {
+ // positive case
+ PackageName: "java_se",
+ Constraint: version.MustGetConstraint("< 8.0.401", version.JVMFormat),
+ Reference: vulnerability.Reference{ID: "CVE-2024-20919-post-jep223", Namespace: "nvd:cpe"},
+ CPEs: cpes,
+ },
+ {
+ // negative case
+ PackageName: "java_se",
+ Constraint: version.MustGetConstraint("< 8.0.399", version.JVMFormat),
+ Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-range-post-jep223", Namespace: "nvd:cpe"},
+ CPEs: cpes,
+ },
+ {
+ // negative case
+ PackageName: "java_se",
+ Constraint: version.MustGetConstraint("< 7.0.0", version.JVMFormat),
+ Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-range-post-jep223", Namespace: "nvd:cpe"},
+ CPEs: cpes,
+ },
+ }...)
+}
diff --git a/grype/pkg/apk_metadata.go b/grype/pkg/apk_metadata.go
new file mode 100644
index 00000000000..5a0ac001e4a
--- /dev/null
+++ b/grype/pkg/apk_metadata.go
@@ -0,0 +1,10 @@
+package pkg
+
+type ApkMetadata struct {
+ Files []ApkFileRecord `json:"files"`
+}
+
+// ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records).
+type ApkFileRecord struct {
+ Path string `json:"path"`
+}
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/cpe_provider.go b/grype/pkg/cpe_provider.go
new file mode 100644
index 00000000000..2e4e7bccb6e
--- /dev/null
+++ b/grype/pkg/cpe_provider.go
@@ -0,0 +1,99 @@
+package pkg
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/anchore/grype/grype/internal"
+ "github.com/anchore/syft/syft/cpe"
+ "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/sbom"
+ "github.com/anchore/syft/syft/source"
+)
+
+const cpeInputPrefix = "cpe:"
+
+type CPELiteralMetadata struct {
+ CPE string
+}
+
+func cpeProvider(userInput string) ([]Package, Context, *sbom.SBOM, error) {
+ reader, ctx, err := getCPEReader(userInput)
+ if err != nil {
+ return nil, Context{}, nil, err
+ }
+
+ return decodeCPEsFromReader(reader, ctx)
+}
+
+func getCPEReader(userInput string) (r io.Reader, ctx Context, err error) {
+ if strings.HasPrefix(userInput, cpeInputPrefix) {
+ ctx.Source = &source.Description{
+ Metadata: CPELiteralMetadata{
+ CPE: userInput,
+ },
+ }
+ return strings.NewReader(userInput), ctx, nil
+ }
+ return nil, ctx, errDoesNotProvide
+}
+
+func decodeCPEsFromReader(reader io.Reader, ctx Context) ([]Package, Context, *sbom.SBOM, error) {
+ scanner := bufio.NewScanner(reader)
+ var packages []Package
+ var syftPkgs []pkg.Package
+
+ for scanner.Scan() {
+ rawLine := scanner.Text()
+ p, syftPkg, err := cpeToPackage(rawLine)
+ if err != nil {
+ return nil, Context{}, nil, err
+ }
+
+ 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...),
+ },
+ }
+
+ return packages, ctx, s, nil
+}
+
+func cpeToPackage(rawLine string) (*Package, *pkg.Package, error) {
+ c, err := cpe.New(rawLine, "")
+ if err != nil {
+ return nil, nil, fmt.Errorf("unable to decode cpe %q: %w", rawLine, err)
+ }
+
+ syftPkg := pkg.Package{
+ Name: c.Attributes.Product,
+ Version: c.Attributes.Version,
+ CPEs: []cpe.CPE{c},
+ Type: internal.CPETargetSoftwareToPackageType(c.Attributes.TargetSW),
+ }
+
+ syftPkg.SetID()
+
+ return &Package{
+ ID: ID(c.Attributes.BindToFmtString()),
+ CPEs: syftPkg.CPEs,
+ Name: syftPkg.Name,
+ Version: syftPkg.Version,
+ Type: syftPkg.Type,
+ Language: syftPkg.Language,
+ }, &syftPkg, nil
+}
diff --git a/grype/pkg/cpe_provider_test.go b/grype/pkg/cpe_provider_test.go
new file mode 100644
index 00000000000..bbc5b0bdf21
--- /dev/null
+++ b/grype/pkg/cpe_provider_test.go
@@ -0,0 +1,169 @@
+package pkg
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/syft/syft/cpe"
+ "github.com/anchore/syft/syft/file"
+ "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/sbom"
+ "github.com/anchore/syft/syft/source"
+)
+
+func Test_CPEProvider(t *testing.T) {
+ tests := []struct {
+ name string
+ userInput string
+ context Context
+ pkgs []Package
+ sbom *sbom.SBOM
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "takes a single cpe",
+ userInput: "cpe:/a:apache:log4j:2.14.1",
+ context: Context{
+ Source: &source.Description{
+ Metadata: CPELiteralMetadata{
+ CPE: "cpe:/a:apache:log4j:2.14.1",
+ },
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "log4j",
+ Version: "2.14.1",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:/a:apache:log4j:2.14.1", ""),
+ },
+ },
+ },
+ sbom: &sbom.SBOM{
+ Artifacts: sbom.Artifacts{
+ Packages: pkg.NewCollection(pkg.Package{
+ Name: "log4j",
+ Version: "2.14.1",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:/a:apache:log4j:2.14.1", ""),
+ },
+ }),
+ },
+ },
+ },
+ {
+ name: "takes cpe with no version",
+ userInput: "cpe:/a:apache:log4j",
+ context: Context{
+ Source: &source.Description{
+ Metadata: CPELiteralMetadata{
+ CPE: "cpe:/a:apache:log4j",
+ },
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "log4j",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:/a:apache:log4j", ""),
+ },
+ },
+ },
+ sbom: &sbom.SBOM{
+ Artifacts: sbom.Artifacts{
+ Packages: pkg.NewCollection(pkg.Package{
+ Name: "log4j",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:/a:apache:log4j", ""),
+ },
+ }),
+ },
+ },
+ },
+ {
+ name: "takes CPE 2.3 format",
+ userInput: "cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*",
+ context: Context{
+ Source: &source.Description{
+ Metadata: CPELiteralMetadata{
+ CPE: "cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*",
+ },
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "log4j",
+ Version: "2.14.1",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""),
+ },
+ },
+ },
+ sbom: &sbom.SBOM{
+ Artifacts: sbom.Artifacts{
+ Packages: pkg.NewCollection(pkg.Package{
+ Name: "log4j",
+ Version: "2.14.1",
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""),
+ },
+ }),
+ },
+ },
+ },
+
+ {
+ name: "invalid prefix",
+ userInput: "dir:test-fixtures/cpe",
+ wantErr: require.Error,
+ },
+ }
+
+ opts := []cmp.Option{
+ cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Metadata", "Type", "Language"),
+ }
+
+ syftPkgOpts := []cmp.Option{
+ cmpopts.IgnoreFields(pkg.Package{}, "id", "Type", "Language"),
+ 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 := cpeProvider(tc.userInput)
+
+ tc.wantErr(t, err)
+ if err != nil {
+ require.Nil(t, packages)
+ return
+ }
+
+ if d := cmp.Diff(tc.context, ctx, opts...); 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 != "" {
+ t.Errorf("unexpected package (-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 Package (-want +got):\n%s", d)
+ }
+ }
+ })
+ }
+}
diff --git a/grype/pkg/golang_metadata.go b/grype/pkg/golang_metadata.go
index 0a126afc094..706c9983c9d 100644
--- a/grype/pkg/golang_metadata.go
+++ b/grype/pkg/golang_metadata.go
@@ -1,11 +1,14 @@
package pkg
+import "github.com/anchore/syft/syft/pkg"
+
type GolangBinMetadata struct {
- BuildSettings map[string]string `json:"goBuildSettings,omitempty"`
- GoCompiledVersion string `json:"goCompiledVersion"`
- Architecture string `json:"architecture"`
- H1Digest string `json:"h1Digest,omitempty"`
- MainModule string `json:"mainModule,omitempty"`
+ BuildSettings pkg.KeyValues `json:"goBuildSettings,omitempty" cyclonedx:"goBuildSettings"`
+ GoCompiledVersion string `json:"goCompiledVersion" cyclonedx:"goCompiledVersion"`
+ Architecture string `json:"architecture" cyclonedx:"architecture"`
+ H1Digest string `json:"h1Digest,omitempty" cyclonedx:"h1Digest"`
+ MainModule string `json:"mainModule,omitempty" cyclonedx:"mainModule"`
+ GoCryptoSettings []string `json:"goCryptoSettings,omitempty" cyclonedx:"goCryptoSettings"`
}
type GolangModMetadata struct {
diff --git a/grype/pkg/java_metadata.go b/grype/pkg/java_metadata.go
index 24ba9371787..328c53bdf2b 100644
--- a/grype/pkg/java_metadata.go
+++ b/grype/pkg/java_metadata.go
@@ -1,5 +1,11 @@
package pkg
+import (
+ "github.com/scylladb/go-set/strset"
+
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
type JavaMetadata struct {
VirtualPath string `json:"virtualPath"`
PomArtifactID string `json:"pomArtifactID"`
@@ -12,3 +18,34 @@ type Digest struct {
Algorithm string `json:"algorithm"`
Value string `json:"value"`
}
+
+type JavaVMInstallationMetadata struct {
+ Release JavaVMReleaseMetadata `json:"release,omitempty"`
+}
+
+type JavaVMReleaseMetadata struct {
+ JavaRuntimeVersion string `json:"javaRuntimeVersion,omitempty"`
+ JavaVersion string `json:"javaVersion,omitempty"`
+ FullVersion string `json:"fullVersion,omitempty"`
+ SemanticVersion string `json:"semanticVersion,omitempty"`
+}
+
+func IsJvmPackage(p Package) bool {
+ if _, ok := p.Metadata.(JavaVMInstallationMetadata); ok {
+ return true
+ }
+
+ if p.Type == syftPkg.BinaryPkg {
+ if HasJvmPackageName(p.Name) {
+ return true
+ }
+ }
+
+ return false
+}
+
+var jvmIndications = strset.New("java_se", "jre", "jdk", "zulu", "openjdk", "java", "java/jre", "java/jdk")
+
+func HasJvmPackageName(name string) bool {
+ return jvmIndications.Has(name)
+}
diff --git a/grype/pkg/java_metadata_test.go b/grype/pkg/java_metadata_test.go
new file mode 100644
index 00000000000..1f93622e15b
--- /dev/null
+++ b/grype/pkg/java_metadata_test.go
@@ -0,0 +1,115 @@
+package pkg
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func TestIsJvmPackage(t *testing.T) {
+ tests := []struct {
+ name string
+ pkg Package
+ expected bool
+ }{
+ {
+ name: "binary package with jdk in name set",
+ pkg: Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "jdk",
+ },
+ expected: true,
+ },
+ {
+ name: "binary package with jre in name set",
+ pkg: Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "jre",
+ },
+ expected: true,
+ },
+ {
+ name: "binary package with java_se in name set",
+ pkg: Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "java_se",
+ },
+ expected: true,
+ },
+ {
+ name: "binary package with zulu in name set",
+ pkg: Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "zulu",
+ },
+ expected: true,
+ },
+ {
+ name: "binary package with openjdk in name set",
+ pkg: Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "openjdk",
+ },
+ expected: true,
+ },
+ {
+ name: "binary package from syft (java/jdk",
+ pkg: Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "java/jre",
+ },
+ expected: true,
+ },
+ {
+ name: "binary package from syft (java/jre)",
+ pkg: Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "java/jdk",
+ },
+ expected: true,
+ },
+ {
+ name: "binary package without jvm-related name",
+ pkg: Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "nodejs",
+ },
+ expected: false,
+ },
+ {
+ name: "non-binary package with jvm-related name",
+ pkg: Package{
+ Type: syftPkg.NpmPkg, // we know this could not be a JVM package installation
+ Name: "jdk",
+ },
+ expected: false,
+ },
+ {
+ name: "package with JavaVMInstallationMetadata",
+ pkg: Package{
+ Type: syftPkg.RpmPkg,
+ Name: "random-package",
+ Metadata: JavaVMInstallationMetadata{},
+ },
+ expected: true,
+ },
+ {
+ name: "package without JavaVMInstallationMetadata",
+ pkg: Package{
+ Type: syftPkg.RpmPkg,
+ Name: "non-jvm-package",
+ Metadata: nil,
+ },
+ expected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := IsJvmPackage(tt.pkg)
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
diff --git a/grype/pkg/metadata.go b/grype/pkg/metadata.go
deleted file mode 100644
index ab09e70c1c2..00000000000
--- a/grype/pkg/metadata.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package pkg
-
-// MetadataType represents the data shape stored within pkg.Package.Metadata.
-type MetadataType string
-
-const (
- // this is the full set of data shapes that can be represented within the pkg.Package.Metadata field
-
- UnknownMetadataType MetadataType = "UnknownMetadata"
- JavaMetadataType MetadataType = "JavaMetadata"
- RpmMetadataType MetadataType = "RpmMetadata"
- GolangBinMetadataType MetadataType = "GolangBinMetadata"
- GolangModMetadataType MetadataType = "GolangModMetadata"
-)
diff --git a/grype/pkg/package.go b/grype/pkg/package.go
index 7ee1253c7b9..dc0634eed7e 100644
--- a/grype/pkg/package.go
+++ b/grype/pkg/package.go
@@ -3,14 +3,17 @@ 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/pkg"
+ syftPkg "github.com/anchore/syft/syft/pkg"
cpes "github.com/anchore/syft/syft/pkg/cataloger/common/cpe"
)
@@ -28,22 +31,22 @@ type ID string
// Package represents an application or library that has been bundled into a distributable format.
type Package struct {
- ID ID
- Name string // the package name
- Version string // the version of the package
- Locations file.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)
- Language pkg.Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc)
- Licenses []string
- Type pkg.Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc)
- CPEs []cpe.CPE // all possible Common Platform Enumerators
- PURL string // the Package URL (see https://github.com/package-url/purl-spec)
- Upstreams []UpstreamPackage
- MetadataType MetadataType
- Metadata interface{} // This is NOT 1-for-1 the syft metadata! Only the select data needed for vulnerability matching
+ ID ID
+ Name string // the package name
+ Version string // the version of the package
+ Locations file.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)
+ Language syftPkg.Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc)
+ Distro *distro.Distro // a specific distro this package originated from
+ Licenses []string
+ Type syftPkg.Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc)
+ CPEs []cpe.CPE // all possible Common Platform Enumerators
+ PURL string // the Package URL (see https://github.com/package-url/purl-spec)
+ Upstreams []UpstreamPackage
+ Metadata interface{} // This is NOT 1-for-1 the syft metadata! Only the select data needed for vulnerability matching
}
-func New(p pkg.Package) Package {
- metadataType, metadata, upstreams := dataFromPkg(p)
+func New(p syftPkg.Package, enhancers ...Enhancer) Package {
+ metadata, upstreams := dataFromPkg(p)
licenseObjs := p.Licenses.ToSlice()
// note: this is used for presentation downstream and is a collection, thus should always be allocated
@@ -55,29 +58,39 @@ func New(p pkg.Package) Package {
licenses = []string{}
}
- return Package{
- ID: ID(p.ID()),
- Name: p.Name,
- Version: p.Version,
- Locations: p.Locations,
- Licenses: licenses,
- Language: p.Language,
- Type: p.Type,
- CPEs: p.CPEs,
- PURL: p.PURL,
- Upstreams: upstreams,
- MetadataType: metadataType,
- Metadata: metadata,
+ out := Package{
+ ID: ID(p.ID()),
+ Name: p.Name,
+ Version: p.Version,
+ Locations: p.Locations,
+ Licenses: licenses,
+ Language: p.Language,
+ Type: p.Type,
+ CPEs: p.CPEs,
+ PURL: p.PURL,
+ 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 *pkg.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 []pkg.Package, config SynthesisConfig) []Package {
+func FromPackages(syftpkgs []syftPkg.Package, config SynthesisConfig, enhancers ...Enhancer) []Package {
var pkgs []Package
- var missingCPEs bool
for _, p := range syftpkgs {
if len(p.CPEs) == 0 {
// For SPDX (or any format, really) we may have no CPEs
@@ -85,14 +98,11 @@ func FromPackages(syftpkgs []pkg.Package, config SynthesisConfig) []Package {
p.CPEs = cpes.Generate(p)
} else {
log.Debugf("no CPEs for package: %s", p)
- missingCPEs = true
}
}
- pkgs = append(pkgs, New(p))
- }
- if missingCPEs {
- log.Warnf("some package(s) are missing CPEs. This may result in missing vulnerabilities. You may autogenerate these using: --add-cpes-if-none")
+ pkgs = append(pkgs, New(p, enhancers...))
}
+
return pkgs
}
@@ -101,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 *pkg.Collection, relationships []artifact.Relationship) *pkg.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 {
@@ -109,13 +119,13 @@ func removePackagesByOverlap(catalog *pkg.Collection, relationships []artifact.R
}
}
- out := pkg.NewCollection()
-
+ out := syftPkg.NewCollection()
+ comprehensiveDistroFeed := distroFeedIsComprehensive(distro)
for p := range catalog.Enumerate() {
r, ok := byOverlap[p.ID()]
if ok {
- from, ok := r.From.(pkg.Package)
- if ok && excludePackage(p, from) {
+ from := catalog.Package(r.From.ID())
+ if from != nil && excludePackage(comprehensiveDistroFeed, p, *from) {
continue
}
}
@@ -125,55 +135,154 @@ func removePackagesByOverlap(catalog *pkg.Collection, relationships []artifact.R
return out
}
-func excludePackage(p pkg.Package, parent pkg.Package) bool {
+func excludePackage(comprehensiveDistroFeed bool, p syftPkg.Package, parent syftPkg.Package) bool {
// NOTE: we are not checking the name because we have mismatches like:
// 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
}
- // filter out only binary pkg, empty types, or equal types
- if p.Type != pkg.BinaryPkg && p.Type != "" && p.Type != parent.Type {
+ // If the parent is an OS package and the child is not, exclude the child
+ // for distros that have a comprehensive feed. That is, distros that list
+ // vulnerabilities that aren't fixed. Otherwise, the child package might
+ // be needed for matching.
+ if comprehensiveDistroFeed && isOSPackage(parent) && !isOSPackage(p) {
+ return true
+ }
+
+ // filter out binary packages, even for non-comprehensive distros
+ if p.Type != syftPkg.BinaryPkg {
return false
}
return true
}
-func dataFromPkg(p pkg.Package) (MetadataType, interface{}, []UpstreamPackage) {
+// distroFeedIsComprehensive returns true if the distro feed
+// is comprehensive enough that we can drop packages owned by distro packages
+// before matching.
+func distroFeedIsComprehensive(dst *distro.Distro) bool {
+ // TODO: this mechanism should be re-examined once https://github.com/anchore/grype/issues/1426
+ // is addressed
+ if dst == nil {
+ return false
+ }
+ 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(string(d), dst.Name()) {
+ return true
+ }
+ for _, n := range dst.IDLike {
+ if strings.EqualFold(string(d), n) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// 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'
+var comprehensiveDistros = []distro.Type{
+ distro.Azure,
+ distro.Debian,
+ distro.Mariner,
+ distro.RedHat,
+ distro.Ubuntu,
+}
+
+func isOSPackage(p syftPkg.Package) bool {
+ switch p.Type {
+ case syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.PortagePkg, syftPkg.AlpmPkg, syftPkg.ApkPkg:
+ return true
+ default:
+ return false
+ }
+}
+
+func dataFromPkg(p syftPkg.Package) (any, []UpstreamPackage) {
var metadata interface{}
var upstreams []UpstreamPackage
- var metadataType MetadataType
- switch p.MetadataType {
- case pkg.GolangBinMetadataType, pkg.GolangModMetadataType:
- metadataType, metadata = golangMetadataFromPkg(p)
- case pkg.DpkgMetadataType:
+ // use the metadata to determine the type of package
+ switch p.Metadata.(type) {
+ case syftPkg.GolangModuleEntry, syftPkg.GolangBinaryBuildinfoEntry:
+ metadata = golangMetadataFromPkg(p)
+ case syftPkg.DpkgDBEntry:
+ upstreams = dpkgDataFromPkg(p)
+ case syftPkg.DpkgArchiveEntry:
upstreams = dpkgDataFromPkg(p)
- case pkg.RpmMetadataType:
+ case syftPkg.RpmArchive, syftPkg.RpmDBEntry:
m, u := rpmDataFromPkg(p)
upstreams = u
if m != nil {
metadata = *m
- metadataType = RpmMetadataType
}
- case pkg.JavaMetadataType:
- if m := javaDataFromPkg(p); m != nil {
+ case syftPkg.JavaArchive:
+ if m := javaDataFromPkgMetadata(p); m != nil {
metadata = *m
- metadataType = JavaMetadataType
}
- case pkg.ApkMetadataType:
+ case syftPkg.ApkDBEntry:
+ metadata = apkMetadataFromPkg(p)
upstreams = apkDataFromPkg(p)
+ case syftPkg.JavaVMInstallation:
+ metadata = javaVMDataFromPkg(p)
+ }
+
+ // there are still cases where we could still fill the metadata from other info (such as the PURL)
+ if metadata == nil {
+ if p.Type == syftPkg.JavaPkg {
+ metadata = javaDataFromPkgData(p)
+ }
}
- return metadataType, metadata, upstreams
+
+ return metadata, upstreams
+}
+
+func javaVMDataFromPkg(p syftPkg.Package) any {
+ if value, ok := p.Metadata.(syftPkg.JavaVMInstallation); ok {
+ return JavaVMInstallationMetadata{
+ Release: JavaVMReleaseMetadata{
+ JavaRuntimeVersion: value.Release.JavaRuntimeVersion,
+ JavaVersion: value.Release.JavaVersion,
+ FullVersion: value.Release.FullVersion,
+ SemanticVersion: value.Release.SemanticVersion,
+ },
+ }
+ }
+
+ return nil
+}
+
+func apkMetadataFromPkg(p syftPkg.Package) interface{} {
+ if m, ok := p.Metadata.(syftPkg.ApkDBEntry); ok {
+ metadata := ApkMetadata{}
+
+ fileRecords := make([]ApkFileRecord, 0, len(m.Files))
+ for _, record := range m.Files {
+ r := ApkFileRecord{Path: record.Path}
+ fileRecords = append(fileRecords, r)
+ }
+
+ metadata.Files = fileRecords
+
+ return metadata
+ }
+
+ return nil
}
-func golangMetadataFromPkg(p pkg.Package) (MetadataType, interface{}) {
+func golangMetadataFromPkg(p syftPkg.Package) interface{} {
switch value := p.Metadata.(type) {
- case pkg.GolangBinMetadata:
+ case syftPkg.GolangBinaryBuildinfoEntry:
metadata := GolangBinMetadata{}
if value.BuildSettings != nil {
metadata.BuildSettings = value.BuildSettings
@@ -182,70 +291,103 @@ func golangMetadataFromPkg(p pkg.Package) (MetadataType, interface{}) {
metadata.Architecture = value.Architecture
metadata.H1Digest = value.H1Digest
metadata.MainModule = value.MainModule
- return GolangBinMetadataType, metadata
- case pkg.GolangModMetadata:
+ return metadata
+ case syftPkg.GolangModuleEntry:
metadata := GolangModMetadata{}
metadata.H1Digest = value.H1Digest
- return GolangModMetadataType, metadata
+ return metadata
}
- return "", nil
+ return nil
}
-func dpkgDataFromPkg(p pkg.Package) (upstreams []UpstreamPackage) {
- if value, ok := p.Metadata.(pkg.DpkgMetadata); ok {
+func dpkgDataFromPkg(p syftPkg.Package) (upstreams []UpstreamPackage) {
+ switch value := p.Metadata.(type) {
+ case syftPkg.DpkgDBEntry:
if value.Source != "" {
upstreams = append(upstreams, UpstreamPackage{
Name: value.Source,
Version: value.SourceVersion,
})
}
- } else {
- log.Warnf("unable to extract DPKG metadata for %s", p)
+ case syftPkg.DpkgArchiveEntry:
+ if value.Source != "" {
+ upstreams = append(upstreams, UpstreamPackage{
+ Name: value.Source,
+ Version: value.SourceVersion,
+ })
+ }
+ default:
+ log.Debugf("unable to extract DPKG metadata for %s", p)
}
+
return upstreams
}
-func rpmDataFromPkg(p pkg.Package) (metadata *RpmMetadata, upstreams []UpstreamPackage) {
- if value, ok := p.Metadata.(pkg.RpmMetadata); ok {
- if value.SourceRpm != "" {
- name, version := getNameAndELVersion(value.SourceRpm)
- if name == "" && version == "" {
- log.Warnf("unable to extract name and version from SourceRPM=%q ", value.SourceRpm)
- } else if name != p.Name {
- // don't include matches if the source package name matches the current package name
- upstreams = append(upstreams, UpstreamPackage{
- Name: name,
- Version: version,
- })
- }
+func rpmDataFromPkg(p syftPkg.Package) (metadata *RpmMetadata, upstreams []UpstreamPackage) {
+ switch m := p.Metadata.(type) {
+ case syftPkg.RpmDBEntry:
+ if m.SourceRpm != "" {
+ upstreams = handleSourceRPM(p.Name, m.SourceRpm)
}
metadata = &RpmMetadata{
- Epoch: value.Epoch,
- ModularityLabel: value.ModularityLabel,
+ Epoch: m.Epoch,
+ ModularityLabel: m.ModularityLabel,
+ }
+ case syftPkg.RpmArchive:
+ if m.SourceRpm != "" {
+ upstreams = handleSourceRPM(p.Name, m.SourceRpm)
+ }
+
+ metadata = &RpmMetadata{
+ Epoch: m.Epoch,
+ ModularityLabel: m.ModularityLabel,
}
- } else {
- log.Warnf("unable to extract RPM metadata for %s", p)
}
return metadata, upstreams
}
+func handleSourceRPM(pkgName, sourceRpm string) []UpstreamPackage {
+ var upstreams []UpstreamPackage
+ name, version := getNameAndELVersion(sourceRpm)
+ if name == "" && version == "" {
+ log.Debugf("unable to extract name and version from SourceRPM=%q", sourceRpm)
+ } else if name != pkgName {
+ // don't include matches if the source package name matches the current package name
+ if name != "" && version != "" {
+ upstreams = append(upstreams,
+ UpstreamPackage{
+ Name: name,
+ Version: version,
+ },
+ )
+ }
+ }
+ return upstreams
+}
+
func getNameAndELVersion(sourceRpm string) (string, string) {
groupMatches := stringutil.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm)
version := groupMatches["version"] + "-" + groupMatches["release"]
return groupMatches["name"], version
}
-func javaDataFromPkg(p pkg.Package) (metadata *JavaMetadata) {
- if value, ok := p.Metadata.(pkg.JavaMetadata); ok {
- var artifact, group, name string
+func javaDataFromPkgMetadata(p syftPkg.Package) (metadata *JavaMetadata) {
+ if value, ok := p.Metadata.(syftPkg.JavaArchive); ok {
+ var artifactID, groupID, name string
if value.PomProperties != nil {
- artifact = value.PomProperties.ArtifactID
- group = value.PomProperties.GroupID
+ artifactID = value.PomProperties.ArtifactID
+ groupID = value.PomProperties.GroupID
+ } else {
+ // get the group ID / artifact ID from the PURL
+ artifactID, groupID = javaGroupArtifactIDFromPurl(p.PURL)
}
+
if value.Manifest != nil {
- if n, ok := value.Manifest.Main["Name"]; ok {
- name = n
+ for _, kv := range value.Manifest.Main {
+ if kv.Key == "Name" {
+ name = kv.Value
+ }
}
}
@@ -261,26 +403,50 @@ func javaDataFromPkg(p pkg.Package) (metadata *JavaMetadata) {
metadata = &JavaMetadata{
VirtualPath: value.VirtualPath,
- PomArtifactID: artifact,
- PomGroupID: group,
+ PomArtifactID: artifactID,
+ PomGroupID: groupID,
ManifestName: name,
ArchiveDigests: archiveDigests,
}
- } else {
- log.Warnf("unable to extract Java metadata for %s", p)
}
return metadata
}
-func apkDataFromPkg(p pkg.Package) (upstreams []UpstreamPackage) {
- if value, ok := p.Metadata.(pkg.ApkMetadata); ok {
+func javaDataFromPkgData(p syftPkg.Package) (metadata *JavaMetadata) {
+ switch p.Type {
+ case syftPkg.JavaPkg:
+ artifactID, groupID := javaGroupArtifactIDFromPurl(p.PURL)
+ if artifactID != "" && groupID != "" {
+ metadata = &JavaMetadata{
+ PomArtifactID: artifactID,
+ PomGroupID: groupID,
+ }
+ }
+ default:
+ log.Debugf("unable to extract metadata for %s", p)
+ }
+
+ return metadata
+}
+
+func javaGroupArtifactIDFromPurl(p string) (string, string) {
+ purl, err := packageurl.FromString(p)
+ if err != nil {
+ log.WithFields("purl", purl, "error", err).Debug("unable to parse java PURL")
+ return "", ""
+ }
+ return purl.Name, purl.Namespace
+}
+
+func apkDataFromPkg(p syftPkg.Package) (upstreams []UpstreamPackage) {
+ if value, ok := p.Metadata.(syftPkg.ApkDBEntry); ok {
if value.OriginPackage != "" {
upstreams = append(upstreams, UpstreamPackage{
Name: value.OriginPackage,
})
}
} else {
- log.Warnf("unable to extract APK metadata for %s", p)
+ log.Debugf("unable to extract APK metadata for %s", p)
}
return upstreams
}
@@ -293,3 +459,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 b863d47ce35..622e0be5d9f 100644
--- a/grype/pkg/package_test.go
+++ b/grype/pkg/package_test.go
@@ -5,15 +5,17 @@ import (
"strings"
"testing"
- "github.com/scylladb/go-set"
- "github.com/scylladb/go-set/strset"
"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"
syftFile "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"
+ "github.com/anchore/syft/syft/testutil"
)
func TestNew(t *testing.T) {
@@ -26,8 +28,7 @@ func TestNew(t *testing.T) {
{
name: "alpm package with source info",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.AlpmMetadataType,
- Metadata: syftPkg.AlpmMetadata{
+ Metadata: syftPkg.AlpmDBEntry{
BasePackage: "base-pkg-info",
Package: "pkg-info",
Version: "version-info",
@@ -41,8 +42,7 @@ func TestNew(t *testing.T) {
{
name: "dpkg with source info",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.DpkgMetadataType,
- Metadata: syftPkg.DpkgMetadata{
+ Metadata: syftPkg.DpkgDBEntry{
Package: "pkg-info",
Source: "src-info",
Version: "version-info",
@@ -70,10 +70,77 @@ func TestNew(t *testing.T) {
},
},
{
- name: "rpm with source info",
+ name: "dpkg archive with source info",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.RpmMetadataType,
- Metadata: syftPkg.RpmMetadata{
+ 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{
+ Metadata: syftPkg.RpmArchive{
+ Name: "name-info",
+ Version: "version-info",
+ Epoch: intRef(30),
+ Arch: "arch-info",
+ Release: "release-info",
+ SourceRpm: "sqlite-3.26.0-6.el8.src.rpm",
+ Size: 40,
+ Vendor: "vendor-info",
+ Files: []syftPkg.RpmFileRecord{
+ {
+ Path: "path-info",
+ Mode: 20,
+ Size: 10,
+ Digest: file.Digest{
+ Algorithm: "algo-info",
+ Value: "digest-info",
+ },
+ UserName: "user-info",
+ GroupName: "group-info",
+ Flags: "flag-info",
+ },
+ },
+ },
+ },
+ metadata: RpmMetadata{
+ Epoch: intRef(30),
+ },
+ upstreams: []UpstreamPackage{
+ {
+ Name: "sqlite",
+ Version: "3.26.0-6.el8",
+ },
+ },
+ },
+ {
+ name: "rpm db entry with source info",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.RpmDBEntry{
Name: "name-info",
Version: "version-info",
Epoch: intRef(30),
@@ -82,7 +149,7 @@ func TestNew(t *testing.T) {
SourceRpm: "sqlite-3.26.0-6.el8.src.rpm",
Size: 40,
Vendor: "vendor-info",
- Files: []syftPkg.RpmdbFileRecord{
+ Files: []syftPkg.RpmFileRecord{
{
Path: "path-info",
Mode: 20,
@@ -109,45 +176,48 @@ func TestNew(t *testing.T) {
},
},
{
- name: "rpm with source info that matches the package info",
+ name: "rpm archive with source info that matches the package info",
syftPkg: syftPkg.Package{
- Name: "sqlite",
- MetadataType: syftPkg.RpmMetadataType,
- Metadata: syftPkg.RpmMetadata{
+ Name: "sqlite",
+ Metadata: syftPkg.RpmArchive{
SourceRpm: "sqlite-3.26.0-6.el8.src.rpm",
},
},
metadata: RpmMetadata{},
},
{
- name: "rpm with modularity label",
+ name: "rpm archive with modularity label",
syftPkg: syftPkg.Package{
- Name: "sqlite",
- MetadataType: syftPkg.RpmMetadataType,
- Metadata: syftPkg.RpmMetadata{
+ Name: "sqlite",
+ Metadata: syftPkg.RpmArchive{
SourceRpm: "sqlite-3.26.0-6.el8.src.rpm",
- ModularityLabel: "abc:2",
+ ModularityLabel: strRef("abc:2"),
},
},
- metadata: RpmMetadata{ModularityLabel: "abc:2"},
+ metadata: RpmMetadata{ModularityLabel: strRef("abc:2")},
},
{
name: "java pkg",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.JavaMetadataType,
- Metadata: syftPkg.JavaMetadata{
+ Metadata: syftPkg.JavaArchive{
VirtualPath: "virtual-path-info",
Manifest: &syftPkg.JavaManifest{
- Main: map[string]string{
- "Name": "main-section-name-info",
+ Main: syftPkg.KeyValues{
+ {
+ Key: "Name",
+ Value: "main-section-name-info",
+ },
},
- NamedSections: map[string]map[string]string{
- "named-section": {
- "named-section-key": "named-section-value",
+ Sections: []syftPkg.KeyValues{
+ {
+ {
+ Key: "named-section-key",
+ Value: "named-section-value",
+ },
},
},
},
- PomProperties: &syftPkg.PomProperties{
+ PomProperties: &syftPkg.JavaPomProperties{
Path: "pom-path-info",
Name: "pom-name-info",
GroupID: "pom-group-ID-info",
@@ -177,8 +247,7 @@ func TestNew(t *testing.T) {
{
name: "apk with source info",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.ApkMetadataType,
- Metadata: syftPkg.ApkMetadata{
+ Metadata: syftPkg.ApkDBEntry{
Package: "libcurl-tools",
OriginPackage: "libcurl",
Maintainer: "somone",
@@ -195,13 +264,13 @@ func TestNew(t *testing.T) {
Name: "libcurl",
},
},
+ metadata: ApkMetadata{Files: []ApkFileRecord{}},
},
// the below packages are those that have no metadata or upstream info to parse out
{
name: "npm-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.NpmPackageJSONMetadataType,
- Metadata: syftPkg.NpmPackageJSONMetadata{
+ Metadata: syftPkg.NpmPackage{
Author: "a",
Homepage: "a",
Description: "a",
@@ -212,8 +281,7 @@ func TestNew(t *testing.T) {
{
name: "python-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.PythonPackageMetadataType,
- Metadata: syftPkg.PythonPackageMetadata{
+ Metadata: syftPkg.PythonPackage{
Name: "a",
Version: "a",
Author: "a",
@@ -226,8 +294,7 @@ func TestNew(t *testing.T) {
{
name: "gem-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.GemMetadataType,
- Metadata: syftPkg.GemMetadata{
+ Metadata: syftPkg.RubyGemspec{
Name: "a",
Version: "a",
Homepage: "a",
@@ -237,8 +304,7 @@ func TestNew(t *testing.T) {
{
name: "kb-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.KbPackageMetadataType,
- Metadata: syftPkg.KbPackageMetadata{
+ Metadata: syftPkg.MicrosoftKbPatch{
ProductID: "a",
Kb: "a",
},
@@ -247,8 +313,7 @@ func TestNew(t *testing.T) {
{
name: "rust-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.RustCargoPackageMetadataType,
- Metadata: syftPkg.CargoPackageMetadata{
+ Metadata: syftPkg.RustCargoLockEntry{
Name: "a",
Version: "a",
Source: "a",
@@ -256,19 +321,27 @@ 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{
- MetadataType: syftPkg.GolangBinMetadataType,
- Metadata: syftPkg.GolangBinMetadata{
- BuildSettings: map[string]string{},
+ Metadata: syftPkg.GolangBinaryBuildinfoEntry{
+ BuildSettings: syftPkg.KeyValues{},
GoCompiledVersion: "1.0.0",
H1Digest: "a",
MainModule: "myMainModule",
},
},
metadata: GolangBinMetadata{
- BuildSettings: map[string]string{},
+ BuildSettings: syftPkg.KeyValues{},
GoCompiledVersion: "1.0.0",
H1Digest: "a",
MainModule: "myMainModule",
@@ -277,8 +350,7 @@ func TestNew(t *testing.T) {
{
name: "golang-mod-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.GolangModMetadataType,
- Metadata: syftPkg.GolangModMetadata{
+ Metadata: syftPkg.GolangModuleEntry{
H1Digest: "h1:as234NweNNTNWEtt13nwNENTt",
},
},
@@ -287,30 +359,63 @@ func TestNew(t *testing.T) {
},
},
{
- name: "php-composer-metadata",
+ name: "php-composer-lock-metadata",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.PhpComposerLockEntry{
+ Name: "a",
+ Version: "a",
+ },
+ },
+ },
+ {
+ name: "php-composer-installed-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.PhpComposerJSONMetadataType,
- Metadata: syftPkg.PhpComposerJSONMetadata{
+ Metadata: syftPkg.PhpComposerInstalledEntry{
Name: "a",
Version: "a",
},
},
},
{
- name: "dart-pub-metadata",
+ name: "dart-publock-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.DartPubMetadataType,
- Metadata: syftPkg.DartPubMetadata{
+ Metadata: syftPkg.DartPubspecLockEntry{
Name: "a",
Version: "a",
},
},
},
+ {
+ 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{
- MetadataType: syftPkg.DotnetDepsMetadataType,
- Metadata: syftPkg.DotnetDepsMetadata{
+ Metadata: syftPkg.DotnetDepsEntry{
Name: "a",
Version: "a",
Path: "a",
@@ -322,32 +427,44 @@ func TestNew(t *testing.T) {
{
name: "cpp conan-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.ConanMetadataType,
- Metadata: syftPkg.ConanMetadata{
+ Metadata: syftPkg.ConanfileEntry{
Ref: "catch2/2.13.8",
},
},
},
{
- name: "cpp conan lock metadata",
+ name: "cpp conan v1 lock metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.ConanLockMetadataType,
- Metadata: syftPkg.ConanLockMetadata{
+ Metadata: syftPkg.ConanV1LockEntry{
Ref: "zlib/1.2.12",
- Options: map[string]string{
- "fPIC": "True",
- "shared": "False",
+ Options: syftPkg.KeyValues{
+ {
+ Key: "fPIC",
+ Value: "True",
+ },
+ {
+ Key: "shared",
+ Value: "false",
+ },
},
Path: "all/conanfile.py",
Context: "host",
},
},
},
+ {
+ name: "cpp conan v2 lock metadata",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.ConanV2LockEntry{
+ Ref: "zlib/1.2.12",
+ PackageID: "some-id",
+ },
+ },
+ },
{
name: "cocoapods cocoapods-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.CocoapodsMetadataType,
- Metadata: syftPkg.CocoapodsMetadata{
+ Metadata: syftPkg.CocoaPodfileLockEntry{
Checksum: "123eere234",
},
},
@@ -355,28 +472,32 @@ func TestNew(t *testing.T) {
{
name: "portage-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.PortageMetadataType,
- Metadata: syftPkg.PortageMetadata{
+ Metadata: syftPkg.PortageEntry{
InstalledSize: 1,
Files: []syftPkg.PortageFileRecord{},
},
},
},
{
- name: "hackage-metadata",
+ name: "hackage-stack-lock-metadata",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.HackageStackYamlLockEntry{
+ PkgHash: "some-hash",
+ },
+ },
+ },
+ {
+ name: "hackage-stack-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.HackageMetadataType,
- Metadata: syftPkg.HackageMetadata{
- Name: "hackage",
- Version: "v0.0.1",
+ Metadata: syftPkg.HackageStackYamlEntry{
+ PkgHash: "some-hash",
},
},
},
{
name: "rebar-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.RebarLockMetadataType,
- Metadata: syftPkg.RebarLockMetadata{
+ Metadata: syftPkg.ErlangRebarLockEntry{
Name: "rebar",
Version: "v0.1.1",
},
@@ -385,8 +506,7 @@ func TestNew(t *testing.T) {
{
name: "npm-package-lock-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.NpmPackageLockJSONMetadataType,
- Metadata: syftPkg.NpmPackageLockJSONMetadata{
+ Metadata: syftPkg.NpmPackageLockEntry{
Resolved: "resolved",
Integrity: "sha1:ab7d8979989b7a98d97",
},
@@ -395,8 +515,7 @@ func TestNew(t *testing.T) {
{
name: "mix-lock-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.MixLockMetadataType,
- Metadata: syftPkg.MixLockMetadata{
+ Metadata: syftPkg.ElixirMixLockEntry{
Name: "mix-lock",
Version: "v0.1.2",
},
@@ -405,8 +524,7 @@ func TestNew(t *testing.T) {
{
name: "pipfile-lock-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.PythonPipfileLockMetadataType,
- Metadata: syftPkg.PythonPipfileLockMetadata{
+ Metadata: syftPkg.PythonPipfileLockEntry{
Hashes: []string{
"sha1:ab8v88a8b88d8d8c88b8s765s47",
},
@@ -417,21 +535,19 @@ func TestNew(t *testing.T) {
{
name: "python-requirements-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.PythonRequirementsMetadataType,
- Metadata: syftPkg.PythonRequirementsMetadata{
+ Metadata: syftPkg.PythonRequirementsEntry{
Name: "a",
Extras: []string{"a"},
VersionConstraint: "a",
URL: "a",
- Markers: map[string]string{"a": "a"},
+ Markers: "a",
},
},
},
{
name: "binary-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.BinaryMetadataType,
- Metadata: syftPkg.BinaryMetadata{
+ Metadata: syftPkg.BinarySignature{
Matches: []syftPkg.ClassifierMatch{
{
Classifier: "node",
@@ -443,8 +559,7 @@ func TestNew(t *testing.T) {
{
name: "nix-store-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.NixStoreMetadataType,
- Metadata: syftPkg.NixStoreMetadata{
+ Metadata: syftPkg.NixStoreEntry{
OutputHash: "a",
Output: "a",
Files: []string{
@@ -456,8 +571,7 @@ func TestNew(t *testing.T) {
{
name: "linux-kernel-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.LinuxKernelMetadataType,
- Metadata: syftPkg.LinuxKernelMetadata{
+ Metadata: syftPkg.LinuxKernel{
Name: "a",
Architecture: "a",
Version: "a",
@@ -475,8 +589,7 @@ func TestNew(t *testing.T) {
{
name: "linux-kernel-module-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.LinuxKernelModuleMetadataType,
- Metadata: syftPkg.LinuxKernelModuleMetadata{
+ Metadata: syftPkg.LinuxKernelModule{
Name: "a",
Version: "a",
SourceVersion: "a",
@@ -498,8 +611,7 @@ func TestNew(t *testing.T) {
{
name: "r-description-file-metadata",
syftPkg: syftPkg.Package{
- MetadataType: syftPkg.RDescriptionFileMetadataType,
- Metadata: syftPkg.RDescriptionFileMetadata{
+ Metadata: syftPkg.RDescription{
Title: "a",
Description: "a",
Author: "a",
@@ -514,33 +626,249 @@ func TestNew(t *testing.T) {
},
},
},
+ {
+ name: "dotnet-portable-executable-metadata",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.DotnetPortableExecutableEntry{
+ AssemblyVersion: "a",
+ LegalCopyright: "a",
+ Comments: "a",
+ InternalName: "a",
+ CompanyName: "a",
+ ProductName: "a",
+ ProductVersion: "a",
+ },
+ },
+ },
+ {
+ name: "swift-package-manager-metadata",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.SwiftPackageManagerResolvedEntry{
+ Revision: "a",
+ },
+ },
+ },
+ {
+ name: "swipl-pack-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.SwiplPackEntry{
+ Name: "a",
+ Version: "a",
+ Author: "a",
+ AuthorEmail: "a",
+ Packager: "a",
+ PackagerEmail: "a",
+ Homepage: "a",
+ Dependencies: []string{
+ "a",
+ },
+ },
+ },
+ },
+ {
+ name: "conaninfo-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.ConaninfoEntry{
+ Ref: "a",
+ PackageID: "a",
+ },
+ },
+ },
+ {
+ name: "rust-binary-audit-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.RustBinaryAuditEntry{
+ Name: "a",
+ Version: "a",
+ Source: "a",
+ },
+ },
+ },
+ {
+ name: "python-poetry-lock-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.PythonPoetryLockEntry{Index: "some-index"},
+ },
+ },
+ {
+ name: "yarn-lock-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.YarnLockEntry{
+ Resolved: "some-resolution",
+ Integrity: "some-digest",
+ },
+ },
+ },
+ {
+ name: "wordpress-plugin-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.WordpressPluginEntry{
+ PluginInstallDirectory: "a",
+ Author: "a",
+ AuthorURI: "a",
+ },
+ },
+ },
+ {
+ name: "elf-binary-package",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.ELFBinaryPackageNoteJSONPayload{
+ Type: "a",
+ Vendor: "a",
+ System: "a",
+ SourceRepo: "a",
+ Commit: "a",
+ },
+ },
+ },
+ {
+ name: "php-pecl-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.PhpPeclEntry{
+ Name: "a",
+ Version: "a",
+ License: []string{"a"},
+ },
+ },
+ },
+ {
+ name: "php-pear-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.PhpPearEntry{
+ Name: "a",
+ Version: "a",
+ },
+ },
+ },
+ {
+ name: "lua-rocks-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.LuaRocksPackage{
+ Name: "a",
+ Version: "a",
+ License: "a",
+ Homepage: "a",
+ Description: "a",
+ URL: "a",
+ Dependencies: map[string]string{"b": "c"},
+ },
+ },
+ },
+ {
+ name: "ocaml-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.OpamPackage{
+ Name: "a",
+ Version: "a",
+ Licenses: []string{"a"},
+ URL: "a",
+ Checksums: []string{"a"},
+ Homepage: "a",
+ Dependencies: []string{"a"},
+ },
+ },
+ },
+ {
+ name: "jvm-installation-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.JavaVMInstallation{
+ Release: syftPkg.JavaVMRelease{
+ Implementor: "a",
+ ImplementorVersion: "a",
+ JavaRuntimeVersion: "b",
+ JavaVersion: "c",
+ JavaVersionDate: "a",
+ Libc: "a",
+ Modules: []string{"a"},
+ OsArch: "a",
+ OsName: "a",
+ OsVersion: "a",
+ Source: "a",
+ BuildSource: "a",
+ BuildSourceRepo: "a",
+ SourceRepo: "a",
+ FullVersion: "d",
+ SemanticVersion: "e",
+ BuildInfo: "a",
+ JvmVariant: "a",
+ JvmVersion: "a",
+ ImageType: "a",
+ BuildType: "a",
+ },
+ Files: []string{"a"},
+ },
+ },
+ metadata: JavaVMInstallationMetadata{
+ Release: JavaVMReleaseMetadata{
+ JavaRuntimeVersion: "b",
+ JavaVersion: "c",
+ FullVersion: "d",
+ SemanticVersion: "e",
+ },
+ },
+ },
+ {
+ name: "dotnet-package-lock-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.DotnetPackagesLockEntry{
+ Name: "AutoMapper",
+ Version: "13.0.1",
+ ContentHash: "/Fx1SbJ16qS7dU4i604Sle+U9VLX+WSNVJggk6MupKVkYvvBm4XqYaeFuf67diHefHKHs50uQIS2YEDFhPCakQ==",
+ Type: "Direct",
+ },
+ },
+ },
+ {
+ name: "bitnami-sbom-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.BitnamiSBOMEntry{
+ Name: "a",
+ Version: "1",
+ },
+ },
+ },
+ {
+ name: "terraform-lock-provider-entry",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.TerraformLockProviderEntry{
+ URL: "registry.terraform.io/hashicorp/aws",
+ Version: "5.72.1",
+ Constraints: "> 5.72.0",
+ Hashes: []string{
+ "h1:jhd5O5o0CfZCNEwwN0EiDAzb7ApuFrtxJqa6HXW4EKE=",
+ "zh:0dea6843836e926d33469b48b948744079023816d16a2ff7666bcfb6aa3522d4",
+ "zh:195fa9513f75800a0d62797ebec75ee73e9b8c28d713fe9b63d3b1d1eec129b3",
+ },
+ },
+ },
+ },
+ {
+ name: "pe binary metadata",
+ syftPkg: syftPkg.Package{
+ Metadata: syftPkg.PEBinary{
+ VersionResources: syftPkg.KeyValues{
+ {
+ Key: "k",
+ Value: "k",
+ },
+ },
+ },
+ },
+ },
}
// capture each observed metadata type, we should see all of them relate to what syft provides by the end of testing
- expectedMetadataTypes := set.NewStringSet()
- for _, ty := range syftPkg.AllMetadataTypes {
- expectedMetadataTypes.Add(string(ty))
- }
+ tester := testutil.NewPackageMetadataCompletionTester(t)
// run all of our cases
- observedMetadataTypes := set.NewStringSet()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- if string(test.syftPkg.MetadataType) != "" {
- observedMetadataTypes.Add(string(test.syftPkg.MetadataType))
- }
- assert.Equal(t, test.metadata, New(test.syftPkg).Metadata, "unexpected metadata")
- assert.Equal(t, test.upstreams, New(test.syftPkg).Upstreams, "unexpected upstream")
+ tester.Tested(t, test.syftPkg.Metadata)
+ p := New(test.syftPkg)
+ assert.Equal(t, test.metadata, p.Metadata, "unexpected metadata")
+ assert.Equal(t, test.upstreams, p.Upstreams, "unexpected upstream")
})
}
-
- // did we see all possible metadata types? if not, then there is an uncovered case and this test should error out
- if !expectedMetadataTypes.IsEqual(observedMetadataTypes) {
- t.Errorf("did not observe all possible package metadata types: missing: %+v extra: %+v",
- strset.Difference(expectedMetadataTypes, observedMetadataTypes),
- strset.Difference(observedMetadataTypes, expectedMetadataTypes),
- )
- }
}
func TestFromCollection_DoesNotPanic(t *testing.T) {
@@ -632,10 +960,10 @@ func intRef(i int) *int {
return &i
}
-func Test_RemoveBinaryPackagesByOverlap(t *testing.T) {
+func Test_RemovePackagesByOverlap(t *testing.T) {
tests := []struct {
name string
- sbom catalogRelationships
+ sbom *sbom.SBOM
expectedPackages []string
}{
{
@@ -652,11 +980,25 @@ func Test_RemoveBinaryPackagesByOverlap(t *testing.T) {
[]string{"apk:node@19.2-r1 -> binary:node@19.2"}),
expectedPackages: []string{"apk:go@1.18", "apk:node@19.2-r1"},
},
+ {
+ name: "does not exclude if OS package owns OS package",
+ sbom: catalogWithOverlaps(
+ []string{"rpm:perl@5.3-r1", "rpm:libperl@5.3"},
+ []string{"rpm:perl@5.3-r1 -> rpm:libperl@5.3"}),
+ expectedPackages: []string{"rpm:libperl@5.3", "rpm:perl@5.3-r1"},
+ },
+ {
+ name: "does not exclude if owning package is non-OS",
+ sbom: catalogWithOverlaps(
+ []string{"python:urllib3@1.2.3", "python:otherlib@1.2.3"},
+ []string{"python:urllib3@1.2.3 -> python:otherlib@1.2.3"}),
+ expectedPackages: []string{"python:otherlib@1.2.3", "python:urllib3@1.2.3"},
+ },
{
name: "excludes multiple package by overlap",
sbom: catalogWithOverlaps(
- []string{"apk:go@1.18", "apk:node@19.2-r1", "binary:node@19.2", "apk:python@3.9-r9", ":python@3.9"},
- []string{"apk:node@19.2-r1 -> binary:node@19.2", "apk:python@3.9-r9 -> :python@3.9"}),
+ []string{"apk:go@1.18", "apk:node@19.2-r1", "binary:node@19.2", "apk:python@3.9-r9", "binary:python@3.9"},
+ []string{"apk:node@19.2-r1 -> binary:node@19.2", "apk:python@3.9-r9 -> binary:python@3.9"}),
expectedPackages: []string{"apk:go@1.18", "apk:node@19.2-r1", "apk:python@3.9-r9"},
},
{
@@ -666,10 +1008,60 @@ func Test_RemoveBinaryPackagesByOverlap(t *testing.T) {
[]string{"rpm:node@19.2-r1 -> apk:node@19.2"}),
expectedPackages: []string{"apk:node@19.2", "rpm:node@19.2-r1"},
},
+ {
+ name: "does not exclude if OS package owns OS package",
+ sbom: catalogWithOverlaps(
+ []string{"rpm:perl@5.3-r1", "rpm:libperl@5.3"},
+ []string{"rpm:perl@5.3-r1 -> rpm:libperl@5.3"}),
+ expectedPackages: []string{"rpm:libperl@5.3", "rpm:perl@5.3-r1"},
+ },
+ {
+ name: "does not exclude if owning package is non-OS",
+ sbom: catalogWithOverlaps(
+ []string{"python:urllib3@1.2.3", "python:otherlib@1.2.3"},
+ []string{"python:urllib3@1.2.3 -> python:otherlib@1.2.3"}),
+ expectedPackages: []string{"python:otherlib@1.2.3", "python:urllib3@1.2.3"},
+ },
+ {
+ name: "python bindings for system RPM install",
+ 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: 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.collection, test.sbom.relationships)
+ 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 {
@@ -680,12 +1072,7 @@ func Test_RemoveBinaryPackagesByOverlap(t *testing.T) {
}
}
-type catalogRelationships struct {
- collection *syftPkg.Collection
- relationships []artifact.Relationship
-}
-
-func catalogWithOverlaps(packages []string, overlaps []string) catalogRelationships {
+func catalogWithOverlaps(packages []string, overlaps []string) *sbom.SBOM {
var pkgs []syftPkg.Package
var relationships []artifact.Relationship
@@ -717,7 +1104,7 @@ func catalogWithOverlaps(packages []string, overlaps []string) catalogRelationsh
pkgs = append(pkgs, p)
}
- for _, overlap := range overlaps {
+ for i, overlap := range overlaps {
parts := strings.Split(overlap, "->")
if len(parts) < 2 {
panic("invalid overlap, use -> to specify, e.g.: pkg1->pkg2")
@@ -725,17 +1112,43 @@ func catalogWithOverlaps(packages []string, overlaps []string) catalogRelationsh
from := toPkg(parts[0])
to := toPkg(parts[1])
- relationships = append(relationships, artifact.Relationship{
- From: from,
- To: to,
- Type: artifact.OwnershipByFileOverlapRelationship,
- })
+ // The catalog will type check whether To or From is a pkg.Package or a *pkg.Package.
+ // Previously, there was a bug where Grype assumed that From was always a pkg.Package.
+ // Therefore, intentionally mix pointer and non-pointer packages to prevent Grype from
+ // assuming which is which again. (The correct usage, calling catalog.Package, always
+ // returns a *pkg.Package, and doesn't rely on any type assertion.)
+ if i%2 == 0 {
+ relationships = append(relationships, artifact.Relationship{
+ From: &from,
+ To: &to,
+ Type: artifact.OwnershipByFileOverlapRelationship,
+ })
+ } else {
+ relationships = append(relationships, artifact.Relationship{
+ From: from,
+ To: to,
+ Type: artifact.OwnershipByFileOverlapRelationship,
+ })
+ }
}
catalog := syftPkg.NewCollection(pkgs...)
- return catalogRelationships{
- collection: catalog,
- relationships: relationships,
+ return &sbom.SBOM{
+ Artifacts: sbom.Artifacts{
+ Packages: catalog,
+ },
+ Relationships: relationships,
}
}
+
+func withLinuxRelease(s *sbom.SBOM, id string) *sbom.SBOM {
+ s.Artifacts.LinuxDistribution = &linux.Release{
+ ID: id,
+ }
+ return s
+}
+
+func strRef(s string) *string {
+ return &s
+}
diff --git a/grype/pkg/provider.go b/grype/pkg/provider.go
index a3356b7ace1..e06af83b23e 100644
--- a/grype/pkg/provider.go
+++ b/grype/pkg/provider.go
@@ -6,6 +6,8 @@ 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"
)
@@ -14,22 +16,42 @@ 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)
+ packages, ctx, s, err := provide(userInput, config)
+ if err != nil {
+ return nil, Context{}, nil, err
+ }
+ setContextDistro(packages, &ctx)
+ return packages, ctx, s, nil
+}
+
+// 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) {
- if len(config.Exclusions) > 0 {
- packages, err = filterPackageExclusions(packages, config.Exclusions)
- if err != nil {
- return nil, ctx, s, err
- }
- }
+ log.WithFields("input", userInput).Trace("interpreting input as one or more PURLs")
return packages, ctx, s, err
}
- packages, err = purlProvider(userInput)
+ packages, ctx, s, err = cpeProvider(userInput)
if !errors.Is(err, errDoesNotProvide) {
- return packages, Context{}, s, err
+ log.WithFields("input", userInput).Trace("interpreting input as a CPE")
+ 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)
}
@@ -75,9 +97,35 @@ func locationMatches(location file.Location, exclusion string) (bool, error) {
if err != nil {
return false, err
}
- matchesVirtualPath, err := doublestar.Match(exclusion, location.VirtualPath)
+ matchesVirtualPath, err := doublestar.Match(exclusion, location.AccessPath)
if err != nil {
return false, err
}
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/provider_config.go b/grype/pkg/provider_config.go
index 2fd8d97e55d..978f5da30b7 100644
--- a/grype/pkg/provider_config.go
+++ b/grype/pkg/provider_config.go
@@ -2,7 +2,7 @@ package pkg
import (
"github.com/anchore/stereoscope/pkg/image"
- "github.com/anchore/syft/syft/pkg/cataloger"
+ "github.com/anchore/syft/syft"
)
type ProviderConfig struct {
@@ -11,7 +11,7 @@ type ProviderConfig struct {
}
type SyftProviderConfig struct {
- CatalogingOptions cataloger.Config
+ SBOMOptions *syft.CreateSBOMConfig
RegistryOptions *image.RegistryOptions
Platform string
Exclusions []string
diff --git a/grype/pkg/provider_test.go b/grype/pkg/provider_test.go
index 34dd94432c5..9b913a24d97 100644
--- a/grype/pkg/provider_test.go
+++ b/grype/pkg/provider_test.go
@@ -6,8 +6,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/imagetest"
+ "github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/file"
- "github.com/anchore/syft/syft/pkg/cataloger"
)
func TestProviderLocationExcludes(t *testing.T) {
@@ -16,6 +16,7 @@ func TestProviderLocationExcludes(t *testing.T) {
fixture string
excludes []string
expected []string
+ wantErr assert.ErrorAssertionFunc
}{
{
name: "exclude everything",
@@ -41,17 +42,30 @@ func TestProviderLocationExcludes(t *testing.T) {
excludes: []string{},
expected: []string{"charsets", "tomcat-embed-el"},
},
+ {
+ name: "exclusions must not hide parsing error",
+ fixture: "test-fixtures/bad-sbom.json",
+ excludes: []string{"**/some-glob/*"},
+ wantErr: assert.Error,
+ },
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg := ProviderConfig{
SyftProviderConfig: SyftProviderConfig{
- Exclusions: test.excludes,
- CatalogingOptions: cataloger.DefaultConfig(),
+ Exclusions: test.excludes,
+ SBOMOptions: syft.DefaultCreateSBOMConfig(),
},
}
- pkgs, _, _, _ := Provide(test.fixture, cfg)
+ if test.wantErr == nil {
+ test.wantErr = assert.NoError
+ }
+ pkgs, _, _, err := Provide(test.fixture, cfg)
+ test.wantErr(t, err)
+ if err != nil {
+ return
+ }
var pkgNames []string
@@ -102,8 +116,8 @@ func TestSyftLocationExcludes(t *testing.T) {
userInput := imagetest.GetFixtureImageTarPath(t, test.fixture)
cfg := ProviderConfig{
SyftProviderConfig: SyftProviderConfig{
- Exclusions: test.excludes,
- CatalogingOptions: cataloger.DefaultConfig(),
+ Exclusions: test.excludes,
+ SBOMOptions: syft.DefaultCreateSBOMConfig(),
},
}
pkgs, _, _, err := Provide(userInput, cfg)
diff --git a/grype/pkg/purl_provider.go b/grype/pkg/purl_provider.go
index 8032e545038..740f34960fe 100644
--- a/grype/pkg/purl_provider.go
+++ b/grype/pkg/purl_provider.go
@@ -1,126 +1,46 @@
package pkg
import (
- "bufio"
"fmt"
"io"
- "os"
"strings"
- "github.com/facebookincubator/nvdtools/wfn"
- "github.com/mitchellh/go-homedir"
-
- "github.com/anchore/packageurl-go"
- "github.com/anchore/syft/syft/cpe"
- "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/format"
+ "github.com/anchore/syft/syft/sbom"
+ "github.com/anchore/syft/syft/source"
)
const (
- purlInputPrefix = "purl:"
- cpesQualifierKey = "cpes"
+ purlInputPrefix = "purl:"
+ singlePurlInputPrefix = "pkg:"
)
-type errEmptyPurlFile struct {
- purlFilepath string
-}
-
-func (e errEmptyPurlFile) Error() string {
- return fmt.Sprintf("purl file is empty: %s", e.purlFilepath)
-}
-
-func purlProvider(userInput string) ([]Package, error) {
- p, err := getPurlPackages(userInput)
- return p, err
+type PURLLiteralMetadata struct {
+ PURL string
}
-func getPurlPackages(userInput string) ([]Package, error) {
- reader, err := getPurlReader(userInput)
+func purlProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) {
+ reader, ctx, err := getPurlReader(userInput)
if err != nil {
- return nil, err
+ return nil, Context{}, nil, err
}
- return decodePurlFile(reader)
-}
-
-func decodePurlFile(reader io.Reader) ([]Package, error) {
- scanner := bufio.NewScanner(reader)
- packages := []Package{}
-
- for scanner.Scan() {
- rawLine := scanner.Text()
- purl, err := packageurl.FromString(rawLine)
- if err != nil {
- return nil, fmt.Errorf("unable to decode purl %s: %w", rawLine, err)
- }
-
- cpes := []wfn.Attributes{}
- epoch := "0"
- for _, qualifier := range purl.Qualifiers {
- if qualifier.Key == cpesQualifierKey {
- rawCpes := strings.Split(qualifier.Value, ",")
- for _, rawCpe := range rawCpes {
- c, err := cpe.New(rawCpe)
- if err != nil {
- return nil, fmt.Errorf("unable to decode cpe %s in purl %s: %w", rawCpe, rawLine, err)
- }
- cpes = append(cpes, c)
- }
- }
-
- if qualifier.Key == "epoch" {
- epoch = qualifier.Value
- }
- }
-
- if purl.Type == packageurl.TypeRPM && !strings.HasPrefix(purl.Version, fmt.Sprintf("%s:", epoch)) {
- purl.Version = fmt.Sprintf("%s:%s", epoch, purl.Version)
- }
-
- packages = append(packages, Package{
- ID: ID(purl.String()),
- CPEs: cpes,
- Name: purl.Name,
- Version: purl.Version,
- Type: pkg.TypeByName(purl.Type),
- Language: pkg.LanguageByName(purl.Type),
- PURL: purl.String(),
- })
+ s, _, _, err := format.Decode(reader)
+ if s == nil {
+ return nil, Context{}, nil, fmt.Errorf("unable to decode purl: %w", err)
}
- if err := scanner.Err(); err != nil {
- return nil, err
- }
- return packages, nil
+ return FromCollection(s.Artifacts.Packages, config.SynthesisConfig, purlEnhancers...), ctx, s, nil
}
-func getPurlReader(userInput string) (r io.Reader, err error) {
- if !explicitlySpecifyingPurl(userInput) {
- return nil, errDoesNotProvide
- }
-
- path := strings.TrimPrefix(userInput, purlInputPrefix)
-
- return openPurlFile(path)
-}
-
-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)
- }
-
- if !fileHasContent(f) {
- return nil, errEmptyPurlFile{path}
+func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) {
+ if strings.HasPrefix(userInput, singlePurlInputPrefix) {
+ ctx.Source = &source.Description{
+ Metadata: PURLLiteralMetadata{
+ PURL: userInput,
+ },
+ }
+ return strings.NewReader(userInput), ctx, nil
}
-
- return f, nil
-}
-
-func explicitlySpecifyingPurl(userInput string) bool {
- return strings.HasPrefix(userInput, purlInputPrefix)
+ return nil, ctx, errDoesNotProvide
}
diff --git a/grype/pkg/purl_provider_test.go b/grype/pkg/purl_provider_test.go
index c01ddfca3ad..ca8b7c371b1 100644
--- a/grype/pkg/purl_provider_test.go
+++ b/grype/pkg/purl_provider_test.go
@@ -3,49 +3,323 @@ package pkg
import (
"testing"
- "github.com/stretchr/testify/assert"
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/distro"
+ "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/source"
)
-func Test_PurlProvider_Fails(t *testing.T) {
- //GIVEN
+func Test_PurlProvider(t *testing.T) {
tests := []struct {
name string
userInput string
+ context Context
+ pkgs []Package
+ wantErr require.ErrorAssertionFunc
}{
- {"fails on path with nonexistant file", "purl:tttt/empty.txt"},
- {"fails on invalid path", "purl:~&&"},
- {"fails on empty purl file", "purl:test-fixtures/empty.json"},
- {"fails on invalid purl in file", "purl:test-fixtures/invalid-purl.txt"},
- {"fails on invalid cpe in file", "purl:test-fixtures/invalid-cpe.txt"},
- {"fails on invalid user input", "dir:test-fixtures/empty.json"},
+ {
+ name: "takes a single purl",
+ userInput: "pkg:apk/curl@7.61.1",
+ context: Context{
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{
+ PURL: "pkg:apk/curl@7.61.1",
+ },
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "curl",
+ Version: "7.61.1",
+ Type: pkg.ApkPkg,
+ PURL: "pkg:apk/curl@7.61.1",
+ },
+ },
+ },
+ {
+ name: "java metadata decoded from purl",
+ userInput: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
+ context: Context{
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{
+ PURL: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
+ },
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "commons-lang3",
+ Version: "3.12.0",
+ Type: pkg.JavaPkg,
+ PURL: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
+ Metadata: JavaMetadata{
+ PomArtifactID: "commons-lang3",
+ PomGroupID: "org.apache.commons",
+ },
+ },
+ },
+ },
+ {
+ name: "os with codename",
+ userInput: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
+ context: Context{
+ Distro: &distro.Distro{
+ Type: "debian",
+ IDLike: []string{"debian"},
+ Codename: "jessie", // important!
+ },
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{
+ PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
+ },
+ },
+ },
+ 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-jessie&upstream=sysvinit",
+ Distro: &distro.Distro{Type: distro.Debian, Version: "", Codename: "jessie", IDLike: []string{"debian"}},
+ Upstreams: []UpstreamPackage{
+ {
+ Name: "sysvinit",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "default upstream",
+ userInput: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
+ context: Context{
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{
+ PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
+ },
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "libcrypto3",
+ Version: "3.3.2",
+ Type: pkg.ApkPkg,
+ PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
+ Upstreams: []UpstreamPackage{
+ {
+ Name: "openssl",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "upstream with version",
+ userInput: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", // %40 is @
+ context: Context{
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{
+ PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1",
+ },
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "libcrypto3",
+ Version: "3.3.2",
+ Type: pkg.ApkPkg,
+ PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1",
+ Upstreams: []UpstreamPackage{
+ {
+ Name: "openssl",
+ Version: "3.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: &distro.Distro{
+ Type: "redhat",
+ IDLike: []string{"redhat"},
+ Version: "8.10",
+ },
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{
+ 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",
+ },
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "systemd-x",
+ 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",
+ Version: "239-82.el8_10.2",
+ },
+ },
+ },
+ },
+ },
+ {
+ 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: &distro.Distro{
+ Type: "redhat",
+ IDLike: []string{"redhat"},
+ Version: "8.10",
+ },
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{
+ 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",
+ },
+ },
+ },
+ pkgs: []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",
+ Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Codename: "", IDLike: []string{"redhat"}},
+ Upstreams: []UpstreamPackage{
+ {
+ Name: "dbus",
+ Version: "1.12.8-26.el8",
+ },
+ },
+ },
+ },
+ },
+ {
+ 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: &distro.Distro{
+ Type: "alpine",
+ IDLike: []string{"alpine"},
+ Version: "3.20.3",
+ },
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{
+ PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
+ },
+ },
+ },
+ pkgs: []Package{
+ {
+ 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: "include namespace in name when purl is type Golang",
+ userInput: "pkg:golang/k8s.io/ingress-nginx@v1.11.2",
+ context: Context{
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2"},
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "k8s.io/ingress-nginx",
+ Version: "v1.11.2",
+ Type: pkg.GoModulePkg,
+ PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2",
+ },
+ },
+ },
+ {
+ name: "include complex namespace in name when purl is type Golang",
+ userInput: "pkg:golang/github.com/wazuh/wazuh@v4.5.0",
+ context: Context{
+ Source: &source.Description{
+ Metadata: PURLLiteralMetadata{PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0"},
+ },
+ },
+ pkgs: []Package{
+ {
+ Name: "github.com/wazuh/wazuh",
+ Version: "v4.5.0",
+ Type: pkg.GoModulePkg,
+ PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0",
+ },
+ },
+ },
+ {
+ name: "do not include namespace when given blank input blank",
+ userInput: "pkg:golang/wazuh@v4.5.0",
+ context: Context{
+ Source: &source.Description{
+ 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 purl list input",
+ userInput: "purl:test-fixtures/purl/invalid-purl.txt",
+ wantErr: require.Error,
+ },
+ {
+ name: "invalid prefix",
+ userInput: "dir:test-fixtures/purl",
+ wantErr: require.Error,
+ },
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- //WHEN
- packages, err := purlProvider(tc.userInput)
+ if tc.wantErr == nil {
+ tc.wantErr = require.NoError
+ }
+
+ packages, ctx, _, err := purlProvider(tc.userInput, ProviderConfig{})
+ setContextDistro(packages, &ctx)
+
+ tc.wantErr(t, err)
+ if err != nil {
+ require.Nil(t, packages)
+ return
+ }
- //THEN
- assert.Nil(t, packages)
- assert.Error(t, err)
- assert.NotEqual(t, "", err.Error())
+ 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], diffOpts...); d != "" {
+ t.Errorf("unexpected context (-want +got):\n%s", d)
+ }
+ }
})
}
}
-func Test_CsvProvide(t *testing.T) {
- //GIVEN
- expected := []string{"curl", "ant", "log4j-core"}
-
- //WHEN
- packages, err := purlProvider("purl:test-fixtures/valid-purl.txt")
-
- //THEN
- packageNames := []string{}
- for _, pkg := range packages {
- assert.NotEmpty(t, pkg.ID)
- packageNames = append(packageNames, pkg.Name)
- }
- assert.NoError(t, err)
- assert.Equal(t, expected, packageNames)
+var diffOpts = []cmp.Option{
+ cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Language", "CPEs"),
+ cmpopts.IgnoreUnexported(distro.Distro{}),
}
diff --git a/grype/pkg/qualifier/platformcpe/qualifier.go b/grype/pkg/qualifier/platformcpe/qualifier.go
index ae33868f64e..ac67fb30943 100644
--- a/grype/pkg/qualifier/platformcpe/qualifier.go
+++ b/grype/pkg/qualifier/platformcpe/qualifier.go
@@ -18,15 +18,15 @@ func New(cpe string) qualifier.Qualifier {
}
func isWindowsPlatformCPE(c cpe.CPE) bool {
- return c.Vendor == "microsoft" && strings.HasPrefix(c.Product, "windows")
+ return c.Attributes.Vendor == "microsoft" && strings.HasPrefix(c.Attributes.Product, "windows")
}
func isUbuntuPlatformCPE(c cpe.CPE) bool {
- if c.Vendor == "canonical" && c.Product == "ubuntu_linux" {
+ if c.Attributes.Vendor == "canonical" && c.Attributes.Product == "ubuntu_linux" {
return true
}
- if c.Vendor == "ubuntu" {
+ if c.Attributes.Vendor == "ubuntu" {
return true
}
@@ -34,19 +34,19 @@ func isUbuntuPlatformCPE(c cpe.CPE) bool {
}
func isDebianPlatformCPE(c cpe.CPE) bool {
- return c.Vendor == "debian" && (c.Product == "debian_linux" || c.Product == "linux")
+ return c.Attributes.Vendor == "debian" && (c.Attributes.Product == "debian_linux" || c.Attributes.Product == "linux")
}
func isWordpressPlatformCPE(c cpe.CPE) bool {
- return c.Vendor == "wordpress" && c.Product == "wordpress"
+ return c.Attributes.Vendor == "wordpress" && c.Attributes.Product == "wordpress"
}
-func (p platformCPE) Satisfied(d *distro.Distro, _ pkg.Package) (bool, error) {
+func (p platformCPE) Satisfied(pk pkg.Package) (bool, error) {
if p.cpe == "" {
return true, nil
}
- c, err := cpe.New(p.cpe)
+ c, err := cpe.New(p.cpe, "")
if err != nil {
return true, err
@@ -59,21 +59,21 @@ func (p platformCPE) Satisfied(d *distro.Distro, _ pkg.Package) (bool, error) {
}
// The remaining checks are on distro, so if the distro is unknown the condition should
- // be considered to be satisified and avoid filtering matches
- if d == nil {
+ // be considered to be satisfied and avoid filtering matches
+ if pk.Distro == nil {
return true, nil
}
if isWindowsPlatformCPE(c) {
- return d.Type == distro.Windows, nil
+ return pk.Distro.Type == distro.Windows, nil
}
if isUbuntuPlatformCPE(c) {
- return d.Type == distro.Ubuntu, nil
+ return pk.Distro.Type == distro.Ubuntu, nil
}
if isDebianPlatformCPE(c) {
- return d.Type == distro.Debian, nil
+ return pk.Distro.Type == distro.Debian, nil
}
return true, err
diff --git a/grype/pkg/qualifier/platformcpe/qualifier_test.go b/grype/pkg/qualifier/platformcpe/qualifier_test.go
index 91f4e6ea3f4..e29c8f80424 100644
--- a/grype/pkg/qualifier/platformcpe/qualifier_test.go
+++ b/grype/pkg/qualifier/platformcpe/qualifier_test.go
@@ -15,155 +15,167 @@ func TestPlatformCPE_Satisfied(t *testing.T) {
name string
platformCPE qualifier.Qualifier
pkg pkg.Package
- distro *distro.Distro
satisfied bool
hasError bool
}{
{
name: "no filter on nil distro",
platformCPE: New("cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: nil,
+ pkg: pkg.Package{},
satisfied: true,
hasError: false,
},
{
name: "no filter when platform CPE is empty",
platformCPE: New(""),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Windows},
- satisfied: true,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Windows},
+ },
+ satisfied: true,
+ hasError: false,
},
{
name: "no filter when platform CPE is invalid",
platformCPE: New(";;;"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Windows},
- satisfied: true,
- hasError: true,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Windows},
+ },
+ satisfied: true,
+ hasError: true,
},
// Windows
{
name: "filter windows platform vuln when distro is not windows",
platformCPE: New("cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Debian},
- satisfied: false,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Debian},
+ },
+ satisfied: false,
+ hasError: false,
},
{
name: "filter windows server platform vuln when distro is not windows",
platformCPE: New("cpe:2.3:o:microsoft:windows_server_2022:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Debian},
- satisfied: false,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Debian},
+ },
+ satisfied: false,
+ hasError: false,
},
{
name: "no filter windows platform vuln when distro is windows",
platformCPE: New("cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Windows},
- satisfied: true,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Windows},
+ },
+ satisfied: true,
+ hasError: false,
},
{
name: "no filter windows server platform vuln when distro is windows",
platformCPE: New("cpe:2.3:o:microsoft:windows_server_2022:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Windows},
- satisfied: true,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Windows},
+ },
+ satisfied: true,
+ hasError: false,
},
// Debian
{
name: "filter debian platform vuln when distro is not debian",
platformCPE: New("cpe:2.3:o:debian:debian_linux:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Ubuntu},
- satisfied: false,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Ubuntu},
+ },
+ satisfied: false,
+ hasError: false,
},
{
name: "filter debian platform vuln when distro is not debian (alternate encountered cpe)",
platformCPE: New("cpe:2.3:o:debian:linux:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.SLES},
- satisfied: false,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.SLES},
+ },
+ satisfied: false,
+ hasError: false,
},
{
name: "no filter debian platform vuln when distro is debian",
platformCPE: New("cpe:2.3:o:debian:debian_linux:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Debian},
- satisfied: true,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Debian},
+ },
+ satisfied: true,
+ hasError: false,
},
{
name: "no filter debian platform vuln when distro is debian (alternate encountered cpe)",
platformCPE: New("cpe:2.3:o:debian:linux:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Debian},
- satisfied: true,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Debian},
+ },
+ satisfied: true,
+ hasError: false,
},
// Ubuntu
{
name: "filter ubuntu platform vuln when distro is not ubuntu",
platformCPE: New("cpe:2.3:o:canonical:ubuntu_linux:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.SLES},
- satisfied: false,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.SLES},
+ },
+ satisfied: false,
+ hasError: false,
},
{
name: "filter ubuntu platform vuln when distro is not ubuntu (alternate encountered cpe)",
platformCPE: New("cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Alpine},
- satisfied: false,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Alpine},
+ },
+ satisfied: false,
+ hasError: false,
},
{
name: "no filter ubuntu platform vuln when distro is ubuntu",
platformCPE: New("cpe:2.3:o:canonical:ubuntu_linux:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Ubuntu},
- satisfied: true,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Ubuntu},
+ },
+ satisfied: true,
+ hasError: false,
},
{
name: "no filter ubuntu platform vuln when distro is ubuntu (alternate encountered cpe)",
platformCPE: New("cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Ubuntu},
- satisfied: true,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Ubuntu},
+ },
+ satisfied: true,
+ hasError: false,
},
// Wordpress
{
name: "always filter wordpress platform vulns (no known distro)",
platformCPE: New("cpe:2.3:o:wordpress:wordpress:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: nil,
+ pkg: pkg.Package{},
satisfied: false,
hasError: false,
},
{
name: "always filter wordpress platform vulns (known distro)",
platformCPE: New("cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- distro: &distro.Distro{Type: distro.Alpine},
- satisfied: false,
- hasError: false,
+ pkg: pkg.Package{
+ Distro: &distro.Distro{Type: distro.Alpine},
+ },
+ satisfied: false,
+ hasError: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- s, err := test.platformCPE.Satisfied(test.distro, test.pkg)
+ s, err := test.platformCPE.Satisfied(test.pkg)
if test.hasError {
assert.Error(t, err)
diff --git a/grype/pkg/qualifier/qualifier.go b/grype/pkg/qualifier/qualifier.go
index 5816c6d02f0..71bf8a94745 100644
--- a/grype/pkg/qualifier/qualifier.go
+++ b/grype/pkg/qualifier/qualifier.go
@@ -1,10 +1,9 @@
package qualifier
import (
- "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/pkg"
)
type Qualifier interface {
- Satisfied(d *distro.Distro, p pkg.Package) (bool, error)
+ Satisfied(p pkg.Package) (bool, error)
}
diff --git a/grype/pkg/qualifier/rpmmodularity/qualifier.go b/grype/pkg/qualifier/rpmmodularity/qualifier.go
index 0071eb33656..cd13131ff3b 100644
--- a/grype/pkg/qualifier/rpmmodularity/qualifier.go
+++ b/grype/pkg/qualifier/rpmmodularity/qualifier.go
@@ -16,33 +16,47 @@ func New(module string) qualifier.Qualifier {
return &rpmModularity{module: module}
}
-func (r rpmModularity) Satisfied(_ *distro.Distro, p pkg.Package) (bool, error) {
- if p.MetadataType == pkg.RpmMetadataType {
+func (r rpmModularity) Satisfied(p pkg.Package) (bool, error) {
+ if p.Metadata == nil {
// If unable to determine package modularity, the constraint should be considered satisfied
- if p.Metadata == nil {
- return true, nil
- }
+ return true, nil
+ }
- m, ok := p.Metadata.(pkg.RpmMetadata)
+ m, ok := p.Metadata.(pkg.RpmMetadata)
+ if !ok {
+ return false, nil
+ }
- // If the package metadata was the rpm type but casting failed
- // we assume it would have been satisfied to
- // avoid dropping potential matches
- if !ok {
- return true, nil
- }
+ if m.ModularityLabel == nil {
+ // If the package modularity is empty (null), the constraint should be considered satisfied.
+ // this is the case where the package source does not support expressing modularity.
+ return true, nil
+ }
- // If the package modularity is empty (""), the constraint should be considered satisfied
- if m.ModularityLabel == "" {
- return true, nil
- }
+ if p.Distro != nil && p.Distro.Type == distro.OracleLinux && *m.ModularityLabel == "" {
+ // For oraclelinux, the default stream of an installed appstream package does not currently set
+ // the MODULARITYLABEL property in the rpm metadata; however, in their advisory data they do specify
+ // modularity information, so this ends up in a case where the vuln entries have modularity but the
+ // packages coming from the sbom won't, so for now we need to treat the constraint as satisfied when the
+ // modularity label from an oraclelinux package is "".
+ // TODO: remove this once we have a way of obtaining and parsing the module information from the DISTTAG
+ // in syft.
+ return true, nil
+ }
- if r.module == "" {
- return false, nil
+ if r.module == "" {
+ if *m.ModularityLabel == "" {
+ // the DB has a modularity label, but it's empty... we also have a modularity label from a package source
+ // that supports being able to express modularity, but it's empty. This is a match.
+ return true, nil
}
- return strings.HasPrefix(m.ModularityLabel, r.module), nil
+ // The package source is able to express modularity, and the DB has a package quality that is empty.
+ // Since we are doing a prefix match against the modularity label (which is guaranteed to be non-empty),
+ // and we are checking for an empty prefix, this will always match, however, semantically this makes no sense.
+ // We don't want package modularities of any value to match this qualifier.
+ return false, nil
}
- return false, nil
+ return strings.HasPrefix(*m.ModularityLabel, r.module), nil
}
diff --git a/grype/pkg/qualifier/rpmmodularity/qualifier_test.go b/grype/pkg/qualifier/rpmmodularity/qualifier_test.go
index 30377e6faec..994876bf760 100644
--- a/grype/pkg/qualifier/rpmmodularity/qualifier_test.go
+++ b/grype/pkg/qualifier/rpmmodularity/qualifier_test.go
@@ -5,11 +5,14 @@ import (
"github.com/stretchr/testify/assert"
+ "github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/pkg/qualifier"
)
func TestRpmModularity_Satisfied(t *testing.T) {
+ oracle, _ := distro.New(distro.OracleLinux, "8", "")
+
tests := []struct {
name string
rpmModularity qualifier.Qualifier
@@ -19,83 +22,138 @@ func TestRpmModularity_Satisfied(t *testing.T) {
{
name: "non rpm metadata",
rpmModularity: New("test:1"),
- pkg: pkg.Package{MetadataType: pkg.UnknownMetadataType},
- satisfied: false,
- },
- {
- name: "invalid rpm metadata",
- rpmModularity: New("test:1"),
- pkg: pkg.Package{MetadataType: pkg.RpmMetadataType, Metadata: pkg.GolangBinMetadata{
- BuildSettings: nil,
- GoCompiledVersion: "",
- Architecture: "",
- H1Digest: "",
- MainModule: "",
- }},
- satisfied: true,
+ pkg: pkg.Package{
+ Distro: nil,
+ Metadata: pkg.JavaMetadata{},
+ },
+ satisfied: false,
},
{
name: "module with package rpm metadata lacking actual metadata 1",
rpmModularity: New("test:1"),
- pkg: pkg.Package{MetadataType: pkg.RpmMetadataType, Metadata: nil},
- satisfied: true,
+ pkg: pkg.Package{
+ Distro: nil,
+ Metadata: nil,
+ },
+ satisfied: true,
},
{
name: "empty module with rpm metadata lacking actual metadata 2",
rpmModularity: New(""),
- pkg: pkg.Package{MetadataType: pkg.RpmMetadataType, Metadata: nil},
+ pkg: pkg.Package{Metadata: nil},
satisfied: true,
},
{
name: "no modularity label with no module",
rpmModularity: New(""),
- pkg: pkg.Package{MetadataType: pkg.RpmMetadataType, Metadata: pkg.RpmMetadata{
- Epoch: nil,
- }},
+ pkg: pkg.Package{
+ Distro: nil,
+ Metadata: pkg.RpmMetadata{
+ Epoch: nil,
+ }},
satisfied: true,
},
{
name: "no modularity label with module",
rpmModularity: New("abc"),
- pkg: pkg.Package{MetadataType: pkg.RpmMetadataType, Metadata: pkg.RpmMetadata{
- Epoch: nil,
- }},
+ pkg: pkg.Package{
+ Distro: nil,
+ Metadata: pkg.RpmMetadata{
+ Epoch: nil,
+ }},
satisfied: true,
},
{
name: "modularity label with no module",
rpmModularity: New(""),
- pkg: pkg.Package{MetadataType: pkg.RpmMetadataType, Metadata: pkg.RpmMetadata{
- Epoch: nil,
- ModularityLabel: "x:3:1234567:abcd",
- }},
+ pkg: pkg.Package{
+ Distro: nil,
+ Metadata: pkg.RpmMetadata{
+ ModularityLabel: strRef("x:3:1234567:abcd"),
+ }},
satisfied: false,
},
{
name: "modularity label in module",
rpmModularity: New("x:3"),
- pkg: pkg.Package{MetadataType: pkg.RpmMetadataType, Metadata: pkg.RpmMetadata{
- Epoch: nil,
- ModularityLabel: "x:3:1234567:abcd",
- }},
+ pkg: pkg.Package{
+ Distro: nil,
+ Metadata: pkg.RpmMetadata{
+ ModularityLabel: strRef("x:3:1234567:abcd"),
+ }},
satisfied: true,
},
{
name: "modularity label not in module",
rpmModularity: New("x:3"),
- pkg: pkg.Package{MetadataType: pkg.RpmMetadataType, Metadata: pkg.RpmMetadata{
- Epoch: nil,
- ModularityLabel: "x:1:1234567:abcd",
- }},
+ pkg: pkg.Package{
+ Distro: nil,
+ Metadata: pkg.RpmMetadata{
+ ModularityLabel: strRef("x:1:1234567:abcd"),
+ }},
+ satisfied: false,
+ },
+ {
+ name: "modularity label is positively blank",
+ rpmModularity: New(""),
+ pkg: pkg.Package{
+ Distro: nil,
+ Metadata: pkg.RpmMetadata{
+ ModularityLabel: strRef(""),
+ }},
+ satisfied: true,
+ },
+ {
+ name: "modularity label is missing (assume we cannot verify that capability)",
+ rpmModularity: New(""),
+ pkg: pkg.Package{
+ Distro: nil,
+ Metadata: pkg.RpmMetadata{
+ ModularityLabel: nil,
+ }},
+ satisfied: true,
+ },
+ {
+ name: "default appstream for oraclelinux (treat as missing)",
+ rpmModularity: New("nodejs:16"),
+ pkg: pkg.Package{
+ Distro: oracle,
+ Metadata: pkg.RpmMetadata{
+ ModularityLabel: strRef(""),
+ }},
+ satisfied: true,
+ },
+ {
+ name: "non-default appstream for oraclelinux matches vuln modularity",
+ rpmModularity: New("nodejs:16"),
+ pkg: pkg.Package{
+ Distro: oracle,
+ Metadata: pkg.RpmMetadata{
+ ModularityLabel: strRef("nodejs:16:blah"),
+ }},
+ satisfied: true,
+ },
+ {
+ name: "non-default appstream for oraclelinux does not match vuln modularity",
+ rpmModularity: New("nodejs:17"),
+ pkg: pkg.Package{
+ Distro: oracle,
+ Metadata: pkg.RpmMetadata{
+ ModularityLabel: strRef("nodejs:16:blah"),
+ }},
satisfied: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- s, err := test.rpmModularity.Satisfied(nil, test.pkg)
+ s, err := test.rpmModularity.Satisfied(test.pkg)
assert.NoError(t, err)
assert.Equal(t, test.satisfied, s)
})
}
}
+
+func strRef(s string) *string {
+ return &s
+}
diff --git a/grype/pkg/rpm_metadata.go b/grype/pkg/rpm_metadata.go
index da0cced5cfd..7905f3a0c3c 100644
--- a/grype/pkg/rpm_metadata.go
+++ b/grype/pkg/rpm_metadata.go
@@ -1,6 +1,6 @@
package pkg
type RpmMetadata struct {
- Epoch *int `json:"epoch"`
- ModularityLabel string `json:"modularityLabel"`
+ Epoch *int `json:"epoch"`
+ ModularityLabel *string `json:"modularityLabel"`
}
diff --git a/grype/pkg/syft_provider.go b/grype/pkg/syft_provider.go
index 8094f9794b5..9bd8a609cd1 100644
--- a/grype/pkg/syft_provider.go
+++ b/grype/pkg/syft_provider.go
@@ -1,10 +1,18 @@
package pkg
import (
+ "context"
+ "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"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
+ "github.com/anchore/syft/syft/source/sourceproviders"
)
func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) {
@@ -12,47 +20,38 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context,
if err != nil {
return nil, Context{}, nil, err
}
+ defer log.CloseAndLogError(src, "syft source")
- defer src.Close()
-
- catalog, relationships, theDistro, err := syft.CatalogPackages(src, config.CatalogingOptions)
+ s, err := syft.CreateSBOM(context.Background(), src, config.SBOMOptions)
if err != nil {
return nil, Context{}, nil, err
}
- catalog = removePackagesByOverlap(catalog, relationships)
+ if s == nil {
+ return nil, Context{}, nil, errors.New("no SBOM provided")
+ }
srcDescription := src.Describe()
- packages := FromCollection(catalog, config.SynthesisConfig)
- context := Context{
- Source: &srcDescription,
- Distro: theDistro,
- }
+ d := distro.FromRelease(s.Artifacts.LinuxDistribution)
+
+ pkgCatalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d)
- sbom := &sbom.SBOM{
- Source: srcDescription,
- Relationships: relationships,
- Artifacts: sbom.Artifacts{
- Packages: catalog,
- },
+ packages := FromCollection(pkgCatalog, config.SynthesisConfig)
+ pkgCtx := Context{
+ Source: &srcDescription,
+ Distro: d,
}
- return packages, context, sbom, nil
+ return packages, pkgCtx, s, nil
}
func getSource(userInput string, config ProviderConfig) (source.Source, error) {
- if config.CatalogingOptions.Search.Scope == "" {
+ if config.SBOMOptions.Search.Scope == "" {
return nil, errDoesNotProvide
}
- detection, err := source.Detect(userInput, source.DetectConfig{
- DefaultImageSource: config.DefaultImagePullSource,
- })
- if err != nil {
- return nil, err
- }
-
+ var err error
var platform *image.Platform
if config.Platform != "" {
platform, err = image.NewPlatform(config.Platform)
@@ -61,14 +60,22 @@ func getSource(userInput string, config ProviderConfig) (source.Source, error) {
}
}
- return detection.NewSource(source.DetectionSourceConfig{
- Alias: source.Alias{
- Name: config.Name,
- },
- RegistryOptions: config.RegistryOptions,
- Platform: platform,
- Exclude: source.ExcludeConfig{
- Paths: config.Exclusions,
- },
- })
+ var sources []string
+ schemeSource, newUserInput := stereoscope.ExtractSchemeSource(userInput, allSourceTags()...)
+ if schemeSource != "" {
+ sources = []string{schemeSource}
+ userInput = newUserInput
+ }
+
+ return syft.GetSource(context.Background(), userInput, syft.DefaultGetSourceConfig().
+ WithSources(sources...).
+ WithDefaultImagePullSource(config.DefaultImagePullSource).
+ WithAlias(source.Alias{Name: config.Name}).
+ WithRegistryOptions(config.RegistryOptions).
+ WithPlatform(platform).
+ WithExcludeConfig(source.ExcludeConfig{Paths: config.Exclusions}))
+}
+
+func allSourceTags() []string {
+ return collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All("", nil)...).Tags()
}
diff --git a/grype/pkg/syft_sbom_provider.go b/grype/pkg/syft_sbom_provider.go
index e316925c684..f2ebf9bbd48 100644
--- a/grype/pkg/syft_sbom_provider.go
+++ b/grype/pkg/syft_sbom_provider.go
@@ -2,194 +2,154 @@ package pkg
import (
"bytes"
+ "errors"
"fmt"
"io"
"os"
"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"
+ "github.com/anchore/syft/syft/format"
+ "github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/sbom"
)
-type errEmptySBOM struct {
- sbomFilepath string
-}
-
-func (e errEmptySBOM) Error() string {
- return fmt.Sprintf("SBOM file is empty: %s", e.sbomFilepath)
+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 := s.Artifacts.Packages
- catalog = removePackagesByOverlap(catalog, s.Relationships)
+ src := s.Source
+ if src.Metadata == nil && path != "" {
+ src.Metadata = SBOMFileMetadata{
+ Path: path,
+ }
+ }
- return FromCollection(catalog, config.SynthesisConfig), Context{
- Source: &s.Source,
- Distro: s.Artifacts.LinuxDistribution,
- }, s, nil
-}
+ d := distro.FromRelease(s.Artifacts.LinuxDistribution)
-func newInputInfo(scheme, contentTye string) *inputInfo {
- return &inputInfo{
- Scheme: scheme,
- ContentType: contentTye,
+ catalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d)
+
+ 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)
- if err != nil {
- return nil, err
- }
-
- s, format, err := syft.Decode(reader)
+func getSBOM(userInput string) (*sbom.SBOM, sbom.FormatID, string, error) {
+ reader, path, err := getSBOMReader(userInput)
if err != nil {
- return nil, fmt.Errorf("unable to decode sbom: %w", err)
- }
-
- if format == nil {
- return nil, errDoesNotProvide
+ return nil, "", path, err
}
- return s, nil
+ s, fmtID, err := readSBOM(reader)
+ return s, fmtID, path, err
}
-func getSBOMReader(userInput string) (r io.Reader, err error) {
- r, _, err = extractReaderAndInfo(userInput)
+func readSBOM(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, error) {
+ s, fmtID, _, err := format.Decode(reader)
if err != nil {
- return nil, err
+ return nil, "", fmt.Errorf("unable to decode sbom: %w", err)
}
- return r, nil
+ if fmtID == "" || s == nil {
+ return nil, "", errDoesNotProvide
+ }
+
+ return s, fmtID, nil
}
-func extractReaderAndInfo(userInput string) (io.Reader, *inputInfo, error) {
+func getSBOMReader(userInput string) (io.ReadSeeker, string, error) {
switch {
// the order of cases matter
case userInput == "":
// we only want to attempt reading in from stdin if the user has not specified other
// options from the CLI, otherwise we should not assume there is any valid input from stdin.
- return decodeStdin(stdinReader())
+ r, err := stdinReader()
+ if err != nil {
+ 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
- }
-}
-
-func parseSBOM(scheme, path string) (io.Reader, *inputInfo, error) {
- r, err := openFile(path)
- if err != nil {
- return nil, nil, err
+ return nil, "", errDoesNotProvide
}
- info := newInputInfo(scheme, "sbom")
- return r, info, nil
}
-func decodeStdin(r io.Reader) (io.Reader, *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
}
-// fileHasContent returns a bool indicating whether the given file has data that could possibly be utilized in
-// downstream processing.
-func fileHasContent(f *os.File) bool {
- if f == nil {
- return false
- }
-
- info, err := f.Stat()
- if err != nil {
- return false
- }
-
- if size := info.Size(); size > 0 {
- return true
- }
-
- return false
-}
-
-func stdinReader() io.Reader {
+func stdinReader() (io.Reader, error) {
isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect()
if err != nil {
- log.Warnf("unable to determine if there is piped input: %+v", err)
- return nil
+ return nil, fmt.Errorf("unable to determine if there is piped input: %w", err)
}
if !isStdinPipeOrRedirect {
- return nil
+ return nil, errors.New("no input was provided via stdin")
}
- return os.Stdin
+ 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)
- }
-
- if !fileHasContent(f) {
- return nil, errEmptySBOM{path}
+ 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 {
@@ -213,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 4d1b0a12164..5d18d06570c 100644
--- a/grype/pkg/syft_sbom_provider_test.go
+++ b/grype/pkg/syft_sbom_provider_test.go
@@ -1,17 +1,19 @@
package pkg
import (
- "os"
+ "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"
)
@@ -39,7 +41,7 @@ func TestParseSyftJSON(t *testing.T) {
},
Type: "apk",
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:alpine:alpine_baselayout:3.2.0-r6:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:alpine:alpine_baselayout:3.2.0-r6:*:*:*:*:*:*:*", ""),
},
PURL: "pkg:alpine/alpine-baselayout@3.2.0-r6?arch=x86_64",
Upstreams: []UpstreamPackage{
@@ -47,6 +49,107 @@ func TestParseSyftJSON(t *testing.T) {
Name: "alpine-baselayout",
},
},
+ Metadata: ApkMetadata{
+ Files: []ApkFileRecord{
+ {Path: "/dev"},
+ {Path: "/dev/pts"},
+ {Path: "/dev/shm"},
+ {Path: "/etc"},
+ {Path: "/etc/fstab"},
+ {Path: "/etc/group"},
+ {Path: "/etc/hostname"},
+ {Path: "/etc/hosts"},
+ {Path: "/etc/inittab"},
+ {Path: "/etc/modules"},
+ {Path: "/etc/motd"},
+ {Path: "/etc/mtab"},
+ {Path: "/etc/passwd"},
+ {Path: "/etc/profile"},
+ {Path: "/etc/protocols"},
+ {Path: "/etc/services"},
+ {Path: "/etc/shadow"},
+ {Path: "/etc/shells"},
+ {Path: "/etc/sysctl.conf"},
+ {Path: "/etc/apk"},
+ {Path: "/etc/conf.d"},
+ {Path: "/etc/crontabs"},
+ {Path: "/etc/crontabs/root"},
+ {Path: "/etc/init.d"},
+ {Path: "/etc/modprobe.d"},
+ {Path: "/etc/modprobe.d/aliases.conf"},
+ {Path: "/etc/modprobe.d/blacklist.conf"},
+ {Path: "/etc/modprobe.d/i386.conf"},
+ {Path: "/etc/modprobe.d/kms.conf"},
+ {Path: "/etc/modules-load.d"},
+ {Path: "/etc/network"},
+ {Path: "/etc/network/if-down.d"},
+ {Path: "/etc/network/if-post-down.d"},
+ {Path: "/etc/network/if-pre-up.d"},
+ {Path: "/etc/network/if-up.d"},
+ {Path: "/etc/opt"},
+ {Path: "/etc/periodic"},
+ {Path: "/etc/periodic/15min"},
+ {Path: "/etc/periodic/daily"},
+ {Path: "/etc/periodic/hourly"},
+ {Path: "/etc/periodic/monthly"},
+ {Path: "/etc/periodic/weekly"},
+ {Path: "/etc/profile.d"},
+ {Path: "/etc/profile.d/README"},
+ {Path: "/etc/profile.d/color_prompt.sh.disabled"},
+ {Path: "/etc/profile.d/locale.sh"},
+ {Path: "/etc/sysctl.d"},
+ {Path: "/home"},
+ {Path: "/lib"},
+ {Path: "/lib/firmware"},
+ {Path: "/lib/mdev"},
+ {Path: "/lib/modules-load.d"},
+ {Path: "/lib/sysctl.d"},
+ {Path: "/lib/sysctl.d/00-alpine.conf"},
+ {Path: "/media"},
+ {Path: "/media/cdrom"},
+ {Path: "/media/floppy"},
+ {Path: "/media/usb"},
+ {Path: "/mnt"},
+ {Path: "/opt"},
+ {Path: "/proc"},
+ {Path: "/root"},
+ {Path: "/run"},
+ {Path: "/sbin"},
+ {Path: "/sbin/mkmntdirs"},
+ {Path: "/srv"},
+ {Path: "/sys"},
+ {Path: "/tmp"},
+ {Path: "/usr"},
+ {Path: "/usr/lib"},
+ {Path: "/usr/lib/modules-load.d"},
+ {Path: "/usr/local"},
+ {Path: "/usr/local/bin"},
+ {Path: "/usr/local/lib"},
+ {Path: "/usr/local/share"},
+ {Path: "/usr/sbin"},
+ {Path: "/usr/share"},
+ {Path: "/usr/share/man"},
+ {Path: "/usr/share/misc"},
+ {Path: "/var"},
+ {Path: "/var/run"},
+ {Path: "/var/cache"},
+ {Path: "/var/cache/misc"},
+ {Path: "/var/empty"},
+ {Path: "/var/lib"},
+ {Path: "/var/lib/misc"},
+ {Path: "/var/local"},
+ {Path: "/var/lock"},
+ {Path: "/var/lock/subsys"},
+ {Path: "/var/log"},
+ {Path: "/var/mail"},
+ {Path: "/var/opt"},
+ {Path: "/var/spool"},
+ {Path: "/var/spool/mail"},
+ {Path: "/var/spool/cron"},
+ {Path: "/var/spool/cron/crontabs"},
+ {Path: "/var/tmp"},
+ },
+ },
},
{
Name: "fake",
@@ -63,8 +166,8 @@ func TestParseSyftJSON(t *testing.T) {
},
Type: "dpkg",
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:*:fake:1.2.0:*:*:*:*:*:*:*"),
- cpe.Must("cpe:2.3:a:fake:fake:1.2.0:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:*:fake:1.2.0:*:*:*:*:*:*:*", ""),
+ cpe.Must("cpe:2.3:a:fake:fake:1.2.0:*:*:*:*:*:*:*", ""),
},
PURL: "pkg:deb/debian/fake@1.2.0?arch=x86_64",
Upstreams: []UpstreamPackage{
@@ -89,11 +192,10 @@ func TestParseSyftJSON(t *testing.T) {
},
Type: "java-archive",
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:*:gmp:6.2.0-r0:*:*:*:*:*:*:*"),
- cpe.Must("cpe:2.3:a:gmp:gmp:6.2.0-r0:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:a:*:gmp:6.2.0-r0:*:*:*:*:*:*:*", ""),
+ cpe.Must("cpe:2.3:a:gmp:gmp:6.2.0-r0:*:*:*:*:*:*:*", ""),
},
- PURL: "pkg:alpine/gmp@6.2.0-r0?arch=x86_64",
- MetadataType: JavaMetadataType,
+ PURL: "pkg:alpine/gmp@6.2.0-r0?arch=x86_64",
Metadata: JavaMetadata{
PomArtifactID: "aid",
PomGroupID: "gid",
@@ -103,9 +205,9 @@ func TestParseSyftJSON(t *testing.T) {
},
Context: Context{
Source: &source.Description{
- Metadata: source.StereoscopeImageSourceMetadata{
+ Metadata: source.ImageMetadata{
UserInput: "alpine:fake",
- Layers: []source.StereoscopeLayerMetadata{
+ Layers: []source.LayerMetadata{
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a",
@@ -121,8 +223,8 @@ func TestParseSyftJSON(t *testing.T) {
},
},
},
- Distro: &linux.Release{
- Name: "alpine",
+ Distro: &distro.Distro{
+ Type: "alpine",
Version: "3.12.0",
},
},
@@ -137,7 +239,7 @@ func TestParseSyftJSON(t *testing.T) {
t.Fatalf("unable to parse: %+v", err)
}
- if m, ok := context.Source.Metadata.(source.StereoscopeImageSourceMetadata); ok {
+ if m, ok := context.Source.Metadata.(source.ImageMetadata); ok {
m.RawConfig = nil
m.RawManifest = nil
@@ -192,12 +294,11 @@ var springImageTestCase = struct {
Licenses: []string{},
Type: "java-archive",
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:charsets:charsets:*:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:a:charsets:charsets:*:*:*:*:*:maven:*:*"),
+ cpe.Must("cpe:2.3:a:charsets:charsets:*:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:a:charsets:charsets:*:*:*:*:*:maven:*:*", ""),
},
- PURL: "",
- MetadataType: JavaMetadataType,
- Metadata: JavaMetadata{VirtualPath: "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar"},
+ PURL: "",
+ Metadata: JavaMetadata{VirtualPath: "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar"},
},
{
Name: "tomcat-embed-el",
@@ -212,19 +313,18 @@ var springImageTestCase = struct {
Licenses: []string{},
Type: "java-archive",
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:tomcat_embed_el:tomcat-embed-el:9.0.27:*:*:*:*:java:*:*"),
- cpe.Must("cpe:2.3:a:tomcat-embed-el:tomcat_embed_el:9.0.27:*:*:*:*:maven:*:*"),
+ cpe.Must("cpe:2.3:a:tomcat_embed_el:tomcat-embed-el:9.0.27:*:*:*:*:java:*:*", ""),
+ cpe.Must("cpe:2.3:a:tomcat-embed-el:tomcat_embed_el:9.0.27:*:*:*:*:maven:*:*", ""),
},
- PURL: "",
- MetadataType: JavaMetadataType,
- Metadata: JavaMetadata{VirtualPath: "/app/libs/tomcat-embed-el-9.0.27.jar"},
+ PURL: "",
+ Metadata: JavaMetadata{VirtualPath: "/app/libs/tomcat-embed-el-9.0.27.jar"},
},
},
Context: Context{
Source: &source.Description{
- Metadata: source.StereoscopeImageSourceMetadata{
+ Metadata: source.ImageMetadata{
UserInput: "springio/gs-spring-boot-docker:latest",
- Layers: []source.StereoscopeLayerMetadata{
+ Layers: []source.LayerMetadata{
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:42a3027eaac150d2b8f516100921f4bd83b3dbc20bfe64124f686c072b49c602",
@@ -241,24 +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 TestGetSBOMReader_EmptySBOM(t *testing.T) {
- sbomFile, err := os.CreateTemp("", "empty.sbom")
- require.NoError(t, err)
- defer func() {
- err := sbomFile.Close()
- assert.NoError(t, err)
- }()
+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
+ }
- filepath := sbomFile.Name()
- userInput := "sbom:" + filepath
+ packages, ctx, _, err := Provide(tc.userInput, ProviderConfig{})
- _, err = getSBOMReader(userInput)
- assert.ErrorAs(t, err, &errEmptySBOM{})
+ 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/db/test-fixtures/archives/metadata.json b/grype/pkg/test-fixtures/bad-sbom.json
similarity index 100%
rename from grype/db/test-fixtures/archives/metadata.json
rename to grype/pkg/test-fixtures/bad-sbom.json
diff --git a/grype/pkg/test-fixtures/purl/different-os.txt b/grype/pkg/test-fixtures/purl/different-os.txt
new file mode 100644
index 00000000000..43a3b446bd5
--- /dev/null
+++ b/grype/pkg/test-fixtures/purl/different-os.txt
@@ -0,0 +1,2 @@
+pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3
+pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2
diff --git a/grype/pkg/test-fixtures/empty.json b/grype/pkg/test-fixtures/purl/empty.json
similarity index 100%
rename from grype/pkg/test-fixtures/empty.json
rename to grype/pkg/test-fixtures/purl/empty.json
diff --git a/grype/pkg/test-fixtures/purl/homogeneous-os.txt b/grype/pkg/test-fixtures/purl/homogeneous-os.txt
new file mode 100644
index 00000000000..b7383632c75
--- /dev/null
+++ b/grype/pkg/test-fixtures/purl/homogeneous-os.txt
@@ -0,0 +1,2 @@
+pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3
+pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3
diff --git a/grype/pkg/test-fixtures/invalid-cpe.txt b/grype/pkg/test-fixtures/purl/invalid-cpe.txt
similarity index 100%
rename from grype/pkg/test-fixtures/invalid-cpe.txt
rename to grype/pkg/test-fixtures/purl/invalid-cpe.txt
diff --git a/grype/pkg/test-fixtures/invalid-purl.txt b/grype/pkg/test-fixtures/purl/invalid-purl.txt
similarity index 100%
rename from grype/pkg/test-fixtures/invalid-purl.txt
rename to grype/pkg/test-fixtures/purl/invalid-purl.txt
diff --git a/grype/pkg/test-fixtures/purl/valid-purl.txt b/grype/pkg/test-fixtures/purl/valid-purl.txt
new file mode 100644
index 00000000000..b06f91da4e8
--- /dev/null
+++ b/grype/pkg/test-fixtures/purl/valid-purl.txt
@@ -0,0 +1,3 @@
+pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit
+pkg:maven/org.apache.ant/ant@1.10.8
+pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1
diff --git a/grype/pkg/test-fixtures/valid-purl.txt b/grype/pkg/test-fixtures/valid-purl.txt
deleted file mode 100644
index fe82c18ed6a..00000000000
--- a/grype/pkg/test-fixtures/valid-purl.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie&cpes=cpe%3A2.3%3Aa%3Abenjamin_curtis%3Aphpbugtracker%3A1.0.3%3A%2A%3A%2A%3A%2A%3A%2A%3A%2A%3A%2A%3A%2A
-pkg:maven/org.apache.ant/ant@1.10.8
-pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1
diff --git a/grype/pkg/upstream_package.go b/grype/pkg/upstream_package.go
index 15d5b7d4991..73326cda1ac 100644
--- a/grype/pkg/upstream_package.go
+++ b/grype/pkg/upstream_package.go
@@ -32,10 +32,10 @@ func UpstreamPackages(p Package) (pkgs []Package) {
cpeStrings := strset.New()
for _, c := range tmp.CPEs {
if u.Version != "" {
- c.Version = u.Version
+ c.Attributes.Version = u.Version
}
- updatedCPEString := strings.ReplaceAll(c.BindToFmtString(), p.Name, u.Name)
+ updatedCPEString := strings.ReplaceAll(c.Attributes.BindToFmtString(), p.Name, u.Name)
cpeStrings.Add(updatedCPEString)
}
@@ -43,7 +43,7 @@ func UpstreamPackages(p Package) (pkgs []Package) {
// with each entry in set, convert string to CPE and update the new CPEs
var updatedCPEs []cpe.CPE
for _, cpeString := range cpeStrings.List() {
- updatedCPE, _ := cpe.New(cpeString)
+ updatedCPE, _ := cpe.New(cpeString, "")
updatedCPEs = append(updatedCPEs, updatedCPE)
}
tmp.CPEs = updatedCPEs
diff --git a/grype/pkg/upstream_package_test.go b/grype/pkg/upstream_package_test.go
index bf059e79059..05a96652f78 100644
--- a/grype/pkg/upstream_package_test.go
+++ b/grype/pkg/upstream_package_test.go
@@ -28,7 +28,7 @@ func TestUpstreamPackages(t *testing.T) {
Name: "name",
Version: "version",
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name:name:version:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name:name:version:*:*:*:*:*:*:*", ""),
},
Upstreams: []UpstreamPackage{
{
@@ -42,7 +42,7 @@ func TestUpstreamPackages(t *testing.T) {
Version: "version", // original
CPEs: []cpe.CPE{
// name and vendor replaced
- cpe.Must("cpe:2.3:*:new-name:new-name:version:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:new-name:new-name:version:*:*:*:*:*:*:*", ""),
},
// no upstreams
},
@@ -54,7 +54,7 @@ func TestUpstreamPackages(t *testing.T) {
Name: "name",
Version: "version",
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name:name:version:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name:name:version:*:*:*:*:*:*:*", ""),
},
Upstreams: []UpstreamPackage{
{
@@ -69,7 +69,7 @@ func TestUpstreamPackages(t *testing.T) {
Version: "new-version", // new
CPEs: []cpe.CPE{
// name, vendor, and version replaced
- cpe.Must("cpe:2.3:*:new-name:new-name:new-version:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:new-name:new-name:new-version:*:*:*:*:*:*:*", ""),
},
// no upstreams
},
@@ -81,7 +81,7 @@ func TestUpstreamPackages(t *testing.T) {
Name: "name",
Version: "version",
CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:name:name:version:*:*:*:*:*:*:*"),
+ cpe.Must("cpe:2.3:*:name:name:version:*:*:*:*:*:*:*", ""),
},
Upstreams: []UpstreamPackage{
{
@@ -96,9 +96,7 @@ func TestUpstreamPackages(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var actual []Package
- for _, upstream := range UpstreamPackages(tt.pkg) {
- actual = append(actual, upstream)
- }
+ actual = append(actual, UpstreamPackages(tt.pkg)...)
assert.Equalf(t, tt.expected, actual, "UpstreamPackages(%v)", tt.pkg)
})
}
diff --git a/grype/presenter/cyclonedx/presenter.go b/grype/presenter/cyclonedx/presenter.go
index 54b1a957b3f..ab75ce3263d 100644
--- a/grype/presenter/cyclonedx/presenter.go
+++ b/grype/presenter/cyclonedx/presenter.go
@@ -5,79 +5,74 @@ import (
"github.com/CycloneDX/cyclonedx-go"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/clio"
"github.com/anchore/grype/grype/presenter/models"
- "github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/grype/internal"
- "github.com/anchore/grype/internal/version"
- "github.com/anchore/syft/syft/formats/common/cyclonedxhelpers"
+ "github.com/anchore/syft/syft/format/common/cyclonedxhelpers"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
// Presenter writes a CycloneDX report from the given Matches and Scope contents
type Presenter struct {
- results match.Matches
- packages []pkg.Package
- src *source.Description
- metadataProvider vulnerability.MetadataProvider
- format cyclonedx.BOMFileFormat
- sbom *sbom.SBOM
+ id clio.Identification
+ document models.Document
+ src source.Description
+ format cyclonedx.BOMFileFormat
+ sbom *sbom.SBOM
}
-// NewPresenter is a *Presenter constructor
+// NewJSONPresenter is a *Presenter constructor
func NewJSONPresenter(pb models.PresenterConfig) *Presenter {
return &Presenter{
- results: pb.Matches,
- packages: pb.Packages,
- metadataProvider: pb.MetadataProvider,
- src: pb.Context.Source,
- sbom: pb.SBOM,
- format: cyclonedx.BOMFileFormatJSON,
+ id: pb.ID,
+ document: pb.Document,
+ src: pb.SBOM.Source,
+ sbom: pb.SBOM,
+ format: cyclonedx.BOMFileFormatJSON,
}
}
-// NewPresenter is a *Presenter constructor
+// NewXMLPresenter is a *Presenter constructor
func NewXMLPresenter(pb models.PresenterConfig) *Presenter {
return &Presenter{
- results: pb.Matches,
- packages: pb.Packages,
- metadataProvider: pb.MetadataProvider,
- src: pb.Context.Source,
- sbom: pb.SBOM,
- format: cyclonedx.BOMFileFormatXML,
+ id: pb.ID,
+ document: pb.Document,
+ src: pb.SBOM.Source,
+ sbom: pb.SBOM,
+ format: cyclonedx.BOMFileFormatXML,
}
}
// Present creates a CycloneDX-based reporting
-func (pres *Presenter) Present(output io.Writer) error {
+func (p *Presenter) Present(output io.Writer) error {
// note: this uses the syft cyclondx helpers to create
// a consistent cyclondx BOM across syft and grype
- cyclonedxBOM := cyclonedxhelpers.ToFormatModel(*pres.sbom)
+ cyclonedxBOM := cyclonedxhelpers.ToFormatModel(*p.sbom)
// empty the tool metadata and add grype metadata
- versionInfo := version.FromBuild()
- cyclonedxBOM.Metadata.Tools = &[]cyclonedx.Tool{
- {
- Vendor: "anchore",
- Name: internal.ApplicationName,
- Version: versionInfo.Version,
+ cyclonedxBOM.Metadata.Tools = &cyclonedx.ToolsChoice{
+ Components: &[]cyclonedx.Component{
+ {
+ Type: cyclonedx.ComponentTypeApplication,
+ Author: "anchore",
+ Name: p.id.Name,
+ Version: p.id.Version,
+ },
},
}
vulns := make([]cyclonedx.Vulnerability, 0)
- for _, m := range pres.results.Sorted() {
- v, err := NewVulnerability(m, pres.metadataProvider)
+ for _, m := range p.document.Matches {
+ v, err := NewVulnerability(m)
if err != nil {
continue
}
vulns = append(vulns, v)
}
cyclonedxBOM.Vulnerabilities = &vulns
- enc := cyclonedx.NewBOMEncoder(output, pres.format)
+ enc := cyclonedx.NewBOMEncoder(output, p.format)
enc.SetPretty(true)
enc.SetEscapeHTML(false)
- return enc.Encode(cyclonedxBOM)
+ return enc.EncodeVersion(cyclonedxBOM, cyclonedxBOM.SpecVersion)
}
diff --git a/grype/presenter/cyclonedx/presenter_test.go b/grype/presenter/cyclonedx/presenter_test.go
index c0e4debb900..093bb92304e 100644
--- a/grype/presenter/cyclonedx/presenter_test.go
+++ b/grype/presenter/cyclonedx/presenter_test.go
@@ -3,29 +3,117 @@ package cyclonedx
import (
"bytes"
"flag"
+ "fmt"
+ "os/exec"
+ "strings"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/anchore/go-testutils"
"github.com/anchore/grype/grype/presenter/internal"
"github.com/anchore/grype/grype/presenter/models"
+ "github.com/anchore/syft/syft/file"
+ "github.com/anchore/syft/syft/sbom"
)
var update = flag.Bool("update", false, "update the *.golden files for cyclonedx presenters")
+var validatorImage = "cyclonedx/cyclonedx-cli:0.27.2@sha256:829c9ea8f2104698bc3c1228575bfa495f6cc4ec151329323c013ca94408477f"
+
+func Test_CycloneDX_Valid(t *testing.T) {
+ tests := []struct {
+ name string
+ scheme internal.SyftSource
+ }{
+ {
+ name: "json directory",
+ scheme: internal.DirectorySource,
+ },
+ {
+ name: "json image",
+ scheme: internal.ImageSource,
+ },
+ {
+ name: "xml directory",
+ scheme: internal.DirectorySource,
+ },
+ {
+ name: "xml image",
+ scheme: internal.ImageSource,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ format := strings.Split(tc.name, " ")[0]
+ var buffer bytes.Buffer
+
+ pb := internal.GeneratePresenterConfig(t, tc.scheme)
+
+ var pres *Presenter
+ switch format {
+ case "json":
+ pres = NewJSONPresenter(pb)
+ case "xml":
+ pres = NewXMLPresenter(pb)
+ default:
+ t.Fatalf("invalid format: %s", format)
+ }
+
+ err := pres.Present(&buffer)
+ require.NoError(t, err)
+
+ contents := buffer.String()
+
+ cmd := exec.Command("docker", "run", "--rm", "-i", "--entrypoint", "/bin/sh", validatorImage,
+ "-c", fmt.Sprintf("tee &> /dev/null && cyclonedx validate --input-version v1_6 --fail-on-errors --input-format %s", format))
+
+ out := bytes.Buffer{}
+ cmd.Stdout = &out
+ cmd.Stderr = &out
+
+ // pipe to the docker command
+ cmd.Stdin = strings.NewReader(contents)
+
+ err = cmd.Run()
+ if err != nil || cmd.ProcessState.ExitCode() != 0 {
+ // not valid
+ t.Fatalf("error validating CycloneDX %s document: %s \nBOM:\n%s", format, out.String(), contents)
+ }
+ })
+ }
+}
+
+func Test_noTypedNils(t *testing.T) {
+ s := sbom.SBOM{
+ Artifacts: sbom.Artifacts{
+ FileMetadata: map[file.Coordinates]file.Metadata{},
+ FileDigests: map[file.Coordinates][]file.Digest{},
+ },
+ }
+ c := file.NewCoordinates("/file", "123")
+ s.Artifacts.FileMetadata[c] = file.Metadata{
+ Path: "/file",
+ }
+ s.Artifacts.FileDigests[c] = []file.Digest{}
+
+ p := NewJSONPresenter(models.PresenterConfig{
+ SBOM: &s,
+ Pretty: false,
+ })
+ contents := bytes.Buffer{}
+ err := p.Present(&contents)
+ require.NoError(t, err)
+ require.NotContains(t, contents.String(), "null")
+}
func TestCycloneDxPresenterImage(t *testing.T) {
var buffer bytes.Buffer
- matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.ImageSource)
- sbom := internal.SBOMFromPackages(t, packages)
- pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- Context: context,
- MetadataProvider: metadataProvider,
- SBOM: sbom,
- }
+ pb := internal.GeneratePresenterConfig(t, internal.ImageSource)
pres := NewJSONPresenter(pb)
// run presenter
@@ -45,20 +133,15 @@ func TestCycloneDxPresenterImage(t *testing.T) {
actual = internal.Redact(actual)
expected = internal.Redact(expected)
- require.JSONEq(t, string(expected), string(actual))
+ if d := cmp.Diff(string(expected), string(actual)); d != "" {
+ t.Fatalf("diff: %s", d)
+ }
}
func TestCycloneDxPresenterDir(t *testing.T) {
var buffer bytes.Buffer
- matches, packages, ctx, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.DirectorySource)
- sbom := internal.SBOMFromPackages(t, packages)
- pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- Context: ctx,
- MetadataProvider: metadataProvider,
- SBOM: sbom,
- }
+
+ pb := internal.GeneratePresenterConfig(t, internal.DirectorySource)
pres := NewJSONPresenter(pb)
@@ -79,5 +162,7 @@ func TestCycloneDxPresenterDir(t *testing.T) {
actual = internal.Redact(actual)
expected = internal.Redact(expected)
- require.JSONEq(t, string(expected), string(actual))
+ if d := cmp.Diff(string(expected), string(actual)); d != "" {
+ t.Fatalf("diff: %s", d)
+ }
}
diff --git a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden
index 9db52f3b476..3be118a6c54 100644
--- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden
+++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden
@@ -1,43 +1,80 @@
{
- "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
+ "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
- "specVersion": "1.4",
- "serialNumber": "urn:uuid:f701dea7-2715-48eb-8d63-878377007e65",
+ "specVersion": "1.6",
+ "serialNumber": "urn:uuid:c7b9f230-9fb8-43f7-af7a-824cd878a853",
"version": 1,
"metadata": {
- "timestamp": "2023-05-04T09:41:30-04:00",
- "tools": [
- {
- "vendor": "anchore",
- "name": "grype",
- "version": "[not provided]"
- }
- ]
+ "timestamp": "2025-05-16T15:26:00-04:00",
+ "tools": {
+ "components": [
+ {
+ "type": "application",
+ "author": "anchore",
+ "name": "grype",
+ "version": "[not provided]"
+ }
+ ]
+ },
+ "component": {
+ "bom-ref": "163686ac6e30c752",
+ "type": "file",
+ "name": "/var/folders/8y/ct5nbgtj4p30k10kfmq4p4s00000gn/T/TestCycloneDxPresenterDir76562573/001"
+ }
},
"components": [
{
- "bom-ref": "76bd1479d016ce8f",
+ "bom-ref": "bbb0ba712c2b94ea",
"type": "library",
"name": "package-1",
"version": "1.1.1",
- "cpe": "cpe:2.3:a:anchore:engine:0.9.2:*:*:python:*:*:*:*",
+ "cpe": "cpe:2.3:a:anchore:engine:0.9.2:*:*:en:*:*:*:*",
"properties": [
{
"name": "syft:package:type",
"value": "rpm"
},
+ {
+ "name": "syft:package:metadataType",
+ "value": "rpm-db-entry"
+ },
{
"name": "syft:location:0:path",
"value": "/foo/bar/somefile-1.txt"
+ },
+ {
+ "name": "syft:metadata:epoch",
+ "value": "2"
+ },
+ {
+ "name": "syft:metadata:size",
+ "value": "0"
+ },
+ {
+ "name": "syft:metadata:sourceRpm",
+ "value": "some-source-rpm"
}
]
},
{
- "bom-ref": "3199ef19b28ce437",
+ "bom-ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625",
"type": "library",
"name": "package-2",
"version": "2.2.2",
- "cpe": "cpe:2.3:a:anchore:engine:2.2.2:*:*:python:*:*:*:*",
+ "licenses": [
+ {
+ "license": {
+ "id": "Apache-2.0"
+ }
+ },
+ {
+ "license": {
+ "id": "MIT"
+ }
+ }
+ ],
+ "cpe": "cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*",
+ "purl": "pkg:deb/package-2@2.2.2",
"properties": [
{
"name": "syft:package:type",
@@ -52,7 +89,7 @@
],
"vulnerabilities": [
{
- "bom-ref": "urn:uuid:befb74e5-738d-4b2c-adf2-03d276553bca",
+ "bom-ref": "urn:uuid:983f094f-a20c-4aed-b6f1-d9b417c706cc",
"id": "CVE-1999-0001",
"source": {},
"references": [
@@ -63,22 +100,20 @@
],
"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",
- "advisories": [],
"affects": [
{
- "ref": "96699b00fe3004b4"
+ "ref": "bbb0ba712c2b94ea"
}
]
},
{
- "bom-ref": "urn:uuid:9cf43de2-c92a-4f29-add6-29bdd71a0285",
+ "bom-ref": "urn:uuid:ae00e132-f2c5-4d07-896d-31aaf91e04d1",
"id": "CVE-1999-0002",
"source": {},
"references": [
@@ -89,17 +124,15 @@
],
"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",
- "advisories": [],
"affects": [
{
- "ref": "b4013a965511376c"
+ "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 ba5b2c47aaa..96c6d99278a 100644
--- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden
+++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden
@@ -1,43 +1,81 @@
{
- "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
+ "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
- "specVersion": "1.4",
- "serialNumber": "urn:uuid:102e3928-5e9e-4352-bdfe-b9eb64b837f8",
+ "specVersion": "1.6",
+ "serialNumber": "urn:uuid:e9977dcd-e35e-4053-ab49-932f99e4d240",
"version": 1,
"metadata": {
- "timestamp": "2023-05-04T09:41:30-04:00",
- "tools": [
- {
- "vendor": "anchore",
- "name": "grype",
- "version": "[not provided]"
- }
- ]
+ "timestamp": "2025-05-16T15:26:00-04:00",
+ "tools": {
+ "components": [
+ {
+ "type": "application",
+ "author": "anchore",
+ "name": "grype",
+ "version": "[not provided]"
+ }
+ ]
+ },
+ "component": {
+ "bom-ref": "1882f79f937f7d91",
+ "type": "container",
+ "name": "user-input",
+ "version": "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5"
+ }
},
"components": [
{
- "bom-ref": "76bd1479d016ce8f",
+ "bom-ref": "bbb0ba712c2b94ea",
"type": "library",
"name": "package-1",
"version": "1.1.1",
- "cpe": "cpe:2.3:a:anchore:engine:0.9.2:*:*:python:*:*:*:*",
+ "cpe": "cpe:2.3:a:anchore:engine:0.9.2:*:*:en:*:*:*:*",
"properties": [
{
"name": "syft:package:type",
"value": "rpm"
},
+ {
+ "name": "syft:package:metadataType",
+ "value": "rpm-db-entry"
+ },
{
"name": "syft:location:0:path",
"value": "/foo/bar/somefile-1.txt"
+ },
+ {
+ "name": "syft:metadata:epoch",
+ "value": "2"
+ },
+ {
+ "name": "syft:metadata:size",
+ "value": "0"
+ },
+ {
+ "name": "syft:metadata:sourceRpm",
+ "value": "some-source-rpm"
}
]
},
{
- "bom-ref": "3199ef19b28ce437",
+ "bom-ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625",
"type": "library",
"name": "package-2",
"version": "2.2.2",
- "cpe": "cpe:2.3:a:anchore:engine:2.2.2:*:*:python:*:*:*:*",
+ "licenses": [
+ {
+ "license": {
+ "id": "Apache-2.0"
+ }
+ },
+ {
+ "license": {
+ "id": "MIT"
+ }
+ }
+ ],
+ "cpe": "cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*",
+ "purl": "pkg:deb/package-2@2.2.2",
"properties": [
{
"name": "syft:package:type",
@@ -52,7 +90,7 @@
],
"vulnerabilities": [
{
- "bom-ref": "urn:uuid:e082487a-f943-4d4a-8f7c-020d4b0838c4",
+ "bom-ref": "urn:uuid:9f6e21d3-d245-4ee3-9558-cb5cc4936b32",
"id": "CVE-1999-0001",
"source": {},
"references": [
@@ -63,22 +101,20 @@
],
"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",
- "advisories": [],
"affects": [
{
- "ref": "96699b00fe3004b4"
+ "ref": "bbb0ba712c2b94ea"
}
]
},
{
- "bom-ref": "urn:uuid:3d8b0870-5c57-4063-b30d-56102dd49ec1",
+ "bom-ref": "urn:uuid:4fc03529-3b07-41e2-9bc9-c0b2989ccf8e",
"id": "CVE-1999-0002",
"source": {},
"references": [
@@ -89,17 +125,15 @@
],
"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",
- "advisories": [],
"affects": [
{
- "ref": "b4013a965511376c"
+ "ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625"
}
]
}
diff --git a/grype/presenter/cyclonedx/vulnerability.go b/grype/presenter/cyclonedx/vulnerability.go
index 5e8f64e95a1..6fad2e72e90 100644
--- a/grype/presenter/cyclonedx/vulnerability.go
+++ b/grype/presenter/cyclonedx/vulnerability.go
@@ -1,27 +1,21 @@
package cyclonedx
import (
- "fmt"
"strconv"
"strings"
"github.com/CycloneDX/cyclonedx-go"
"github.com/google/uuid"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/presenter/models"
"github.com/anchore/packageurl-go"
)
// https://cyclonedx.org/docs/1.4/json/#vulnerabilities_items_bom-ref
// NewVulnerability creates a Vulnerability document from a match and the metadata provider
-func NewVulnerability(m match.Match, p vulnerability.MetadataProvider) (v cyclonedx.Vulnerability, err error) {
- metadata, err := p.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
- if err != nil {
- return v, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
- }
+func NewVulnerability(m models.Match) (v cyclonedx.Vulnerability, err error) {
+ metadata := m.Vulnerability.VulnerabilityMetadata
ratings := generateCDXRatings(metadata)
@@ -62,7 +56,7 @@ func NewVulnerability(m match.Match, p vulnerability.MetadataProvider) (v cyclon
Advisories: advisories,
Affects: &[]cyclonedx.Affects{
{
- Ref: deriveBomRef(m.Package),
+ Ref: deriveBomRef(m.Artifact),
},
},
// Data source creation
@@ -81,7 +75,7 @@ func NewVulnerability(m match.Match, p vulnerability.MetadataProvider) (v cyclon
}, nil
}
-func generateCDXRatings(metadata *vulnerability.Metadata) []cyclonedx.VulnerabilityRating {
+func generateCDXRatings(metadata models.VulnerabilityMetadata) []cyclonedx.VulnerabilityRating {
severity := cdxSeverityFromGrypeSeverity(metadata.Severity)
ratings := make([]cyclonedx.VulnerabilityRating, 0)
@@ -161,15 +155,15 @@ func cdxSeverityFromGrypeSeverity(severity string) cyclonedx.Severity {
}
}
-func deriveBomRef(p pkg.Package) string {
+func deriveBomRef(p models.Package) string {
// try and parse the PURL if possible and append syft id to it, to make
// the purl unique in the BOM.
// TODO: In the future we may want to dedupe by PURL and combine components with
// the same PURL while preserving their unique metadata.
if parsedPURL, err := packageurl.FromString(p.PURL); err == nil {
- parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "package-id", Value: string(p.ID)})
+ parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "package-id", Value: p.ID})
return parsedPURL.ToString()
}
// fallback is to use strictly the ID if there is no valid pURL
- return string(p.ID)
+ return p.ID
}
diff --git a/grype/presenter/cyclonedx/vulnerability_test.go b/grype/presenter/cyclonedx/vulnerability_test.go
index 44f2b985e77..b734f15a22b 100644
--- a/grype/presenter/cyclonedx/vulnerability_test.go
+++ b/grype/presenter/cyclonedx/vulnerability_test.go
@@ -7,8 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/presenter/models"
"github.com/anchore/grype/grype/vulnerability"
)
@@ -69,11 +68,11 @@ type metadataProvider struct {
cvss []vulnerability.Cvss
}
-func (m metadataProvider) GetMetadata(id, namespace string) (*vulnerability.Metadata, error) {
+func (m metadataProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) {
return &vulnerability.Metadata{
- ID: id,
+ ID: ref.ID,
DataSource: "",
- Namespace: namespace,
+ Namespace: ref.Namespace,
Severity: m.severity,
URLs: nil,
Description: "",
@@ -83,63 +82,64 @@ func (m metadataProvider) GetMetadata(id, namespace string) (*vulnerability.Meta
func TestNewVulnerability_AlwaysIncludesSeverity(t *testing.T) {
tests := []struct {
- name string
- match match.Match
- metadataProvider *metadataProvider
+ name string
+ match models.Match
}{
{
name: "populates severity with missing CVSS records",
- match: match.Match{
- Vulnerability: vulnerability.Vulnerability{},
- Package: pkg.Package{},
- Details: nil,
- },
- metadataProvider: &metadataProvider{
- severity: "High",
+ match: models.Match{
+ Vulnerability: models.Vulnerability{
+ VulnerabilityMetadata: models.VulnerabilityMetadata{
+ Severity: "High",
+ },
+ },
+ Artifact: models.Package{},
+ MatchDetails: nil,
},
},
{
name: "populates severity with all CVSS records",
- match: match.Match{
- Vulnerability: vulnerability.Vulnerability{},
- Package: pkg.Package{},
- Details: nil,
- },
- metadataProvider: &metadataProvider{
- severity: "High",
- cvss: []vulnerability.Cvss{
- {
- Version: "2.0",
- Metrics: vulnerability.CvssMetrics{
- BaseScore: 1.1,
- },
- },
- {
- Version: "3.0",
- Metrics: vulnerability.CvssMetrics{
- BaseScore: 2.1,
- },
- },
- {
- Version: "3.1",
- Metrics: vulnerability.CvssMetrics{
- BaseScore: 3.1,
+ match: models.Match{
+ Vulnerability: models.Vulnerability{
+ VulnerabilityMetadata: models.VulnerabilityMetadata{
+ Severity: "High",
+ Cvss: []models.Cvss{
+ {
+ Version: "2.0",
+ Metrics: models.CvssMetrics{
+ BaseScore: 1.1,
+ },
+ },
+ {
+ Version: "3.0",
+ Metrics: models.CvssMetrics{
+ BaseScore: 2.1,
+ },
+ },
+ {
+ Version: "3.1",
+ Metrics: models.CvssMetrics{
+ BaseScore: 3.1,
+ },
+ },
},
},
},
+ Artifact: models.Package{},
+ MatchDetails: nil,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- actual, err := NewVulnerability(test.match, test.metadataProvider)
+ actual, err := NewVulnerability(test.match)
require.NoError(t, err)
require.NotNil(t, actual.Ratings, "cyclonedx document ratings should not be nil")
require.NotEmpty(t, actual.Ratings)
- require.Equal(t, cdxSeverityFromGrypeSeverity(test.metadataProvider.severity), (*actual.Ratings)[0].Severity)
- if len(test.metadataProvider.cvss) > 0 {
+ require.Equal(t, cdxSeverityFromGrypeSeverity(test.match.Vulnerability.Severity), (*actual.Ratings)[0].Severity)
+ if len(test.match.Vulnerability.Cvss) > 0 {
for i, rating := range *actual.Ratings {
- require.Equal(t, test.metadataProvider.cvss[i].Metrics.BaseScore, *rating.Score)
+ require.Equal(t, test.match.Vulnerability.Cvss[i].Metrics.BaseScore, *rating.Score)
}
}
})
diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap
new file mode 100755
index 00000000000..0d151ca8f8c
--- /dev/null
+++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap
@@ -0,0 +1,108 @@
+
+[TestExplainSnapshot/keycloak-CVE-2020-12413 - 1]
+CVE-2020-12413 from nvd:cpe (Medium)
+The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.
+Related vulnerabilities:
+ - redhat:distro:redhat:9 CVE-2020-12413 (Low)
+Matched packages:
+ - Package: nss, version: 3.79.0-17.el9_1
+ PURL: pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1
+ Match explanation(s):
+ - redhat:distro:redhat:9:CVE-2020-12413 Direct match (package name, version, and ecosystem) against nss (version 3.79.0-17.el9_1).
+ Locations:
+ - /var/lib/rpm/rpmdb.sqlite
+ - Package: nspr, version: 4.34.0-17.el9_1
+ PURL: pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1
+ Match explanation(s):
+ - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.
+ Locations:
+ - /var/lib/rpm/rpmdb.sqlite
+ - Package: nss-softokn, version: 3.79.0-17.el9_1
+ PURL: pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1
+ Match explanation(s):
+ - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.
+ Locations:
+ - /var/lib/rpm/rpmdb.sqlite
+ - Package: nss-softokn-freebl, version: 3.79.0-17.el9_1
+ PURL: pkg:rpm/rhel/nss-softokn-freebl@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1
+ Match explanation(s):
+ - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.
+ Locations:
+ - /var/lib/rpm/rpmdb.sqlite
+ - Package: nss-sysinit, version: 3.79.0-17.el9_1
+ PURL: pkg:rpm/rhel/nss-sysinit@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1
+ Match explanation(s):
+ - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.
+ Locations:
+ - /var/lib/rpm/rpmdb.sqlite
+ - Package: nss-util, version: 3.79.0-17.el9_1
+ PURL: pkg:rpm/rhel/nss-util@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1
+ Match explanation(s):
+ - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.
+ Locations:
+ - /var/lib/rpm/rpmdb.sqlite
+URLs:
+ - https://nvd.nist.gov/vuln/detail/CVE-2020-12413
+ - https://access.redhat.com/security/cve/CVE-2020-12413
+
+---
+
+[TestExplainSnapshot/chainguard-ruby-CVE-2023-28755 - 1]
+CVE-2023-28755 from nvd:cpe (High)
+A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.
+Related vulnerabilities:
+ - github:language:ruby GHSA-hv5j-3h9f-99c2 (High)
+ - wolfi:distro:wolfi:rolling CVE-2023-28755 (High)
+Matched packages:
+ - Package: ruby-3.0, version: 3.0.4-r1
+ PURL: pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118
+ Match explanation(s):
+ - wolfi:distro:wolfi:rolling:CVE-2023-28755 Direct match (package name, version, and ecosystem) against ruby-3.0 (version 3.0.4-r1).
+ - nvd:cpe:CVE-2023-28755 CPE match on `cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*`.
+ - wolfi:distro:wolfi:rolling:CVE-2023-28755 Indirect match; this CVE is reported against ruby-3.0 (version 3.0.4-r1), the upstream of this apk package.
+ Locations:
+ - /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec
+ - /lib/apk/db/installed
+URLs:
+ - https://nvd.nist.gov/vuln/detail/CVE-2023-28755
+ - https://github.com/advisories/GHSA-hv5j-3h9f-99c2
+ - http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755
+
+---
+
+[TestExplainSnapshot/test_a_GHSA - 1]
+GHSA-cfh5-3ghh-wfjx from github:language:java (Medium)
+Moderate severity vulnerability that affects org.apache.httpcomponents:httpclient
+Related vulnerabilities:
+ - nvd:cpe CVE-2014-3577 (Medium)
+Matched packages:
+ - Package: httpclient, version: 4.1.1
+ PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1
+ Match explanation(s):
+ - github:language:java:GHSA-cfh5-3ghh-wfjx Direct match (package name, version, and ecosystem) against httpclient (version 4.1.1).
+ Locations:
+ - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient
+URLs:
+ - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx
+ - https://nvd.nist.gov/vuln/detail/CVE-2014-3577
+
+---
+
+[TestExplainSnapshot/test_a_CVE_alias_of_a_GHSA - 1]
+CVE-2014-3577 from nvd:cpe (Medium)
+org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a "CN=" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the "foo,CN=www.apache.org" string in the O field.
+Related vulnerabilities:
+ - github:language:java GHSA-cfh5-3ghh-wfjx (Medium)
+Matched packages:
+ - Package: httpclient, version: 4.1.1
+ PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1
+ Match explanation(s):
+ - github:language:java:GHSA-cfh5-3ghh-wfjx Direct match (package name, version, and ecosystem) against httpclient (version 4.1.1).
+ - nvd:cpe:CVE-2014-3577 CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*`.
+ Locations:
+ - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient
+URLs:
+ - https://nvd.nist.gov/vuln/detail/CVE-2014-3577
+ - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx
+
+---
diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go
new file mode 100644
index 00000000000..783ad3cc00b
--- /dev/null
+++ b/grype/presenter/explain/explain.go
@@ -0,0 +1,460 @@
+package explain
+
+import (
+ _ "embed"
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+ "text/template"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/presenter/models"
+ "github.com/anchore/syft/syft/file"
+)
+
+//go:embed explain_cve.tmpl
+var explainTemplate string
+
+type VulnerabilityExplainer interface {
+ ExplainByID(IDs []string) error
+ ExplainBySeverity(severity string) error
+ ExplainAll() error
+}
+
+type ViewModel struct {
+ PrimaryVulnerability models.VulnerabilityMetadata
+ RelatedVulnerabilities []models.VulnerabilityMetadata
+ MatchedPackages []*explainedPackage // I think this needs a map of artifacts to explained evidence
+ URLs []string
+}
+
+type viewModelBuilder struct {
+ PrimaryMatch models.Match // The match that seems to be the one we're trying to explain
+ RelatedMatches []models.Match
+ requestedIDs []string // the vulnerability IDs the user requested explanations of
+}
+
+type Findings map[string]ViewModel
+
+type explainedPackage struct {
+ PURL string
+ Name string
+ Version string
+ MatchedOnID string
+ MatchedOnNamespace string
+ IndirectExplanation string
+ DirectExplanation string
+ CPEExplanation string
+ Locations []explainedEvidence
+ displayPriority int // shows how early it should be displayed; direct matches first
+}
+
+type explainedEvidence struct {
+ Location string
+ ArtifactID string
+ ViaVulnID string
+ ViaNamespace string
+}
+
+type vulnerabilityExplainer struct {
+ w io.Writer
+ doc *models.Document
+}
+
+func NewVulnerabilityExplainer(w io.Writer, doc *models.Document) VulnerabilityExplainer {
+ return &vulnerabilityExplainer{
+ w: w,
+ doc: doc,
+ }
+}
+
+var funcs = template.FuncMap{
+ "trim": strings.TrimSpace,
+}
+
+func (e *vulnerabilityExplainer) ExplainByID(ids []string) error {
+ findings, err := Doc(e.doc, ids)
+ if err != nil {
+ return err
+ }
+ t := template.Must(template.New("explanation").Funcs(funcs).Parse(explainTemplate))
+ for _, id := range ids {
+ finding, ok := findings[id]
+ if !ok {
+ continue
+ }
+ if err := t.Execute(e.w, finding); err != nil {
+ return fmt.Errorf("unable to execute template: %w", err)
+ }
+ }
+ return nil
+}
+
+func (e *vulnerabilityExplainer) ExplainBySeverity(_ string) error {
+ return fmt.Errorf("not implemented")
+}
+
+func (e *vulnerabilityExplainer) ExplainAll() error {
+ findings, err := Doc(e.doc, nil)
+ if err != nil {
+ return err
+ }
+ t := template.Must(template.New("explanation").Funcs(funcs).Parse(explainTemplate))
+
+ return t.Execute(e.w, findings)
+}
+
+func Doc(doc *models.Document, requestedIDs []string) (Findings, error) {
+ result := make(Findings)
+ builders := make(map[string]*viewModelBuilder)
+ for _, m := range doc.Matches {
+ key := m.Vulnerability.ID
+ existing, ok := builders[key]
+ if !ok {
+ existing = newBuilder(requestedIDs)
+ builders[m.Vulnerability.ID] = existing
+ }
+ existing.WithMatch(m, requestedIDs)
+ }
+ for _, m := range doc.Matches {
+ for _, related := range m.RelatedVulnerabilities {
+ key := related.ID
+ existing, ok := builders[key]
+ if !ok {
+ existing = newBuilder(requestedIDs)
+ builders[key] = existing
+ }
+ existing.WithMatch(m, requestedIDs)
+ }
+ }
+ for k, v := range builders {
+ result[k] = v.Build()
+ }
+ return result, nil
+}
+
+func newBuilder(requestedIDs []string) *viewModelBuilder {
+ return &viewModelBuilder{
+ requestedIDs: requestedIDs,
+ }
+}
+
+// WithMatch adds a match to the builder
+// accepting enough information to determine whether the match is a primary match or a related match
+func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string) {
+ if b.isPrimaryAdd(m, userRequestedIDs) {
+ // Demote the current primary match to related match
+ // if it exists
+ if b.PrimaryMatch.Vulnerability.ID != "" {
+ b.WithRelatedMatch(b.PrimaryMatch)
+ }
+ b.WithPrimaryMatch(m)
+ } else {
+ b.WithRelatedMatch(m)
+ }
+}
+
+func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string) bool {
+ if b.PrimaryMatch.Vulnerability.ID == "" {
+ return true
+ }
+
+ idWasRequested := false
+ for _, id := range userRequestedIDs {
+ if candidate.Vulnerability.ID == id {
+ idWasRequested = true
+ break
+ }
+ }
+ // the user didn't ask about this ID, so it's not the primary one
+ if !idWasRequested && len(userRequestedIDs) > 0 {
+ return false
+ }
+ // NVD CPEs are somewhat canonical IDs for vulnerabilities, so if the user asked about CVE-YYYY-ID
+ // type number, and we have a record from NVD, consider that the primary record.
+ if candidate.Vulnerability.Namespace == "nvd:cpe" {
+ return true
+ }
+ // Either the user didn't ask for specific IDs, or the candidate has an ID the user asked for.
+ for _, related := range b.PrimaryMatch.RelatedVulnerabilities {
+ if related.ID == candidate.Vulnerability.ID {
+ return true
+ }
+ }
+ return false
+}
+
+func (b *viewModelBuilder) WithPrimaryMatch(m models.Match) *viewModelBuilder {
+ b.PrimaryMatch = m
+ return b
+}
+
+func (b *viewModelBuilder) WithRelatedMatch(m models.Match) *viewModelBuilder {
+ b.RelatedMatches = append(b.RelatedMatches, m)
+ return b
+}
+
+func (b *viewModelBuilder) Build() ViewModel {
+ explainedPackages := groupAndSortEvidence(append(b.RelatedMatches, b.PrimaryMatch))
+
+ var relatedVulnerabilities []models.VulnerabilityMetadata
+ dedupeRelatedVulnerabilities := make(map[string]models.VulnerabilityMetadata)
+ var sortDedupedRelatedVulnerabilities []string
+ for _, m := range append(b.RelatedMatches, b.PrimaryMatch) {
+ key := fmt.Sprintf("%s:%s", m.Vulnerability.Namespace, m.Vulnerability.ID)
+ dedupeRelatedVulnerabilities[key] = m.Vulnerability.VulnerabilityMetadata
+ for _, r := range m.RelatedVulnerabilities {
+ key := fmt.Sprintf("%s:%s", r.Namespace, r.ID)
+ dedupeRelatedVulnerabilities[key] = r
+ }
+ }
+
+ // delete the primary vulnerability from the related vulnerabilities so it isn't listed twice
+ primary := b.primaryVulnerability()
+ delete(dedupeRelatedVulnerabilities, fmt.Sprintf("%s:%s", primary.Namespace, primary.ID))
+ for k := range dedupeRelatedVulnerabilities {
+ sortDedupedRelatedVulnerabilities = append(sortDedupedRelatedVulnerabilities, k)
+ }
+ sort.Strings(sortDedupedRelatedVulnerabilities)
+ for _, k := range sortDedupedRelatedVulnerabilities {
+ relatedVulnerabilities = append(relatedVulnerabilities, dedupeRelatedVulnerabilities[k])
+ }
+
+ return ViewModel{
+ PrimaryVulnerability: primary,
+ RelatedVulnerabilities: relatedVulnerabilities,
+ MatchedPackages: explainedPackages,
+ URLs: b.dedupeAndSortURLs(primary),
+ }
+}
+
+func (b *viewModelBuilder) primaryVulnerability() models.VulnerabilityMetadata {
+ var primaryVulnerability models.VulnerabilityMetadata
+ for _, m := range append(b.RelatedMatches, b.PrimaryMatch) {
+ for _, r := range append(m.RelatedVulnerabilities, m.Vulnerability.VulnerabilityMetadata) {
+ if r.ID == b.PrimaryMatch.Vulnerability.ID && r.Namespace == "nvd:cpe" {
+ primaryVulnerability = r
+ }
+ }
+ }
+ if primaryVulnerability.ID == "" {
+ primaryVulnerability = b.PrimaryMatch.Vulnerability.VulnerabilityMetadata
+ }
+ return primaryVulnerability
+}
+
+// nolint:funlen
+func groupAndSortEvidence(matches []models.Match) []*explainedPackage {
+ idsToMatchDetails := make(map[string]*explainedPackage)
+ for _, m := range matches {
+ key := m.Artifact.ID
+ var newLocations []explainedEvidence
+ for _, l := range m.Artifact.Locations {
+ newLocations = append(newLocations, explainLocation(m, l))
+ }
+ var directExplanation string
+ var indirectExplanation string
+ var cpeExplanation string
+ var matchTypePriority int
+ for i, md := range m.MatchDetails {
+ explanation := explainMatchDetail(m, i)
+ if explanation != "" {
+ switch md.Type {
+ case string(match.CPEMatch):
+ cpeExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation)
+ matchTypePriority = 1 // cpes are a type of direct match
+ case string(match.ExactIndirectMatch):
+ indirectExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation)
+ matchTypePriority = 0 // display indirect matches after direct matches
+ case string(match.ExactDirectMatch):
+ directExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation)
+ matchTypePriority = 2 // exact-direct-matches are high confidence, direct matches; display them first.
+ }
+ }
+ }
+ e, ok := idsToMatchDetails[key]
+ if !ok {
+ e = &explainedPackage{
+ PURL: m.Artifact.PURL,
+ Name: m.Artifact.Name,
+ Version: m.Artifact.Version,
+ MatchedOnID: m.Vulnerability.ID,
+ MatchedOnNamespace: m.Vulnerability.Namespace,
+ DirectExplanation: directExplanation,
+ IndirectExplanation: indirectExplanation,
+ CPEExplanation: cpeExplanation,
+ Locations: newLocations,
+ displayPriority: matchTypePriority,
+ }
+ idsToMatchDetails[key] = e
+ } else {
+ e.Locations = append(e.Locations, newLocations...)
+ if e.CPEExplanation == "" {
+ e.CPEExplanation = cpeExplanation
+ }
+ if e.IndirectExplanation == "" {
+ e.IndirectExplanation = indirectExplanation
+ }
+ e.displayPriority += matchTypePriority
+ }
+ }
+ var sortIDs []string
+ for k, v := range idsToMatchDetails {
+ sortIDs = append(sortIDs, k)
+ dedupeLocations := make(map[string]explainedEvidence)
+ for _, l := range v.Locations {
+ dedupeLocations[l.Location] = l
+ }
+ var uniqueLocations []explainedEvidence
+ for _, l := range dedupeLocations {
+ uniqueLocations = append(uniqueLocations, l)
+ }
+ sort.Slice(uniqueLocations, func(i, j int) bool {
+ if uniqueLocations[i].ViaNamespace == uniqueLocations[j].ViaNamespace {
+ return uniqueLocations[i].Location < uniqueLocations[j].Location
+ }
+ return uniqueLocations[i].ViaNamespace < uniqueLocations[j].ViaNamespace
+ })
+ v.Locations = uniqueLocations
+ }
+
+ sort.Slice(sortIDs, func(i, j int) bool {
+ return explainedPackageIsLess(idsToMatchDetails[sortIDs[i]], idsToMatchDetails[sortIDs[j]])
+ })
+ var explainedPackages []*explainedPackage
+ for _, k := range sortIDs {
+ explainedPackages = append(explainedPackages, idsToMatchDetails[k])
+ }
+ return explainedPackages
+}
+
+func explainedPackageIsLess(i, j *explainedPackage) bool {
+ if i.displayPriority != j.displayPriority {
+ return i.displayPriority > j.displayPriority
+ }
+ return i.Name < j.Name
+}
+
+func explainMatchDetail(m models.Match, index int) string {
+ if len(m.MatchDetails) <= index {
+ return ""
+ }
+ md := m.MatchDetails[index]
+ explanation := ""
+ switch md.Type {
+ case string(match.CPEMatch):
+ explanation = formatCPEExplanation(m)
+ case string(match.ExactIndirectMatch):
+ sourceName, sourceVersion := sourcePackageNameAndVersion(md)
+ explanation = fmt.Sprintf("Indirect match; this CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type)
+ case string(match.ExactDirectMatch):
+ explanation = fmt.Sprintf("Direct match (package name, version, and ecosystem) against %s (version %s).", m.Artifact.Name, m.Artifact.Version)
+ }
+ return explanation
+}
+
+// dedupeAndSortURLs returns a slice of the DataSource fields, deduplicated and sorted
+// the NVD and GHSA URL are given special treatment; they return first and second if present
+// and the rest are sorted by string sort.
+func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.VulnerabilityMetadata) []string {
+ showFirst := primaryVulnerability.DataSource
+ var URLs []string
+ URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource)
+ for _, v := range b.PrimaryMatch.RelatedVulnerabilities {
+ URLs = append(URLs, v.DataSource)
+ }
+ for _, m := range b.RelatedMatches {
+ URLs = append(URLs, m.Vulnerability.DataSource)
+ for _, v := range m.RelatedVulnerabilities {
+ URLs = append(URLs, v.DataSource)
+ }
+ }
+ var result []string
+ deduplicate := make(map[string]bool)
+ result = append(result, showFirst)
+ deduplicate[showFirst] = true
+ nvdURL := ""
+ ghsaURL := ""
+ for _, u := range URLs {
+ if strings.HasPrefix(u, "https://nvd.nist.gov/vuln/detail") {
+ nvdURL = u
+ }
+ if strings.HasPrefix(u, "https://github.com/advisories") {
+ ghsaURL = u
+ }
+ }
+ if nvdURL != "" && nvdURL != showFirst {
+ result = append(result, nvdURL)
+ deduplicate[nvdURL] = true
+ }
+ if ghsaURL != "" && ghsaURL != showFirst {
+ result = append(result, ghsaURL)
+ deduplicate[ghsaURL] = true
+ }
+
+ for _, u := range URLs {
+ if _, ok := deduplicate[u]; !ok {
+ result = append(result, u)
+ deduplicate[u] = true
+ }
+ }
+ return result
+}
+
+func explainLocation(match models.Match, location file.Location) explainedEvidence {
+ path := location.RealPath
+ if javaMeta, ok := match.Artifact.Metadata.(map[string]any); ok {
+ if virtPath, ok := javaMeta["virtualPath"].(string); ok {
+ path = virtPath
+ }
+ }
+ return explainedEvidence{
+ Location: path,
+ ArtifactID: match.Artifact.ID,
+ ViaVulnID: match.Vulnerability.ID,
+ ViaNamespace: match.Vulnerability.Namespace,
+ }
+}
+
+func formatCPEExplanation(m models.Match) string {
+ searchedBy := m.MatchDetails[0].SearchedBy
+ if mapResult, ok := searchedBy.(map[string]interface{}); ok {
+ if cpes, ok := mapResult["cpes"]; ok {
+ if cpeSlice, ok := cpes.([]interface{}); ok {
+ if len(cpeSlice) > 0 {
+ return fmt.Sprintf("CPE match on `%s`.", cpeSlice[0])
+ }
+ }
+ }
+ }
+ return ""
+}
+
+func sourcePackageNameAndVersion(md models.MatchDetails) (string, string) {
+ var name string
+ var version string
+ if mapResult, ok := md.SearchedBy.(map[string]interface{}); ok {
+ if sourcePackage, ok := mapResult["package"]; ok {
+ if sourceMap, ok := sourcePackage.(map[string]interface{}); ok {
+ if maybeName, ok := sourceMap["name"]; ok {
+ name, _ = maybeName.(string)
+ }
+ if maybeVersion, ok := sourceMap["version"]; ok {
+ version, _ = maybeVersion.(string)
+ }
+ }
+ }
+ }
+ return name, version
+}
+
+func nameForUpstream(typ string) string {
+ switch typ {
+ case "deb":
+ return "origin"
+ case "rpm":
+ return "source RPM"
+ }
+ return "upstream"
+}
diff --git a/grype/presenter/explain/explain_cve.tmpl b/grype/presenter/explain/explain_cve.tmpl
new file mode 100644
index 00000000000..952b315350c
--- /dev/null
+++ b/grype/presenter/explain/explain_cve.tmpl
@@ -0,0 +1,15 @@
+{{ .PrimaryVulnerability.ID }} from {{ .PrimaryVulnerability.Namespace }} ({{ .PrimaryVulnerability.Severity }})
+{{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }}
+Related vulnerabilities:{{ range .RelatedVulnerabilities }}
+ - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}}
+Matched packages:{{ range .MatchedPackages }}
+ - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }}
+ PURL: {{ .PURL }}{{ end }}
+ Match explanation(s):{{ if .DirectExplanation }}
+ - {{ .DirectExplanation }}{{ end }}{{ if .CPEExplanation }}
+ - {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }}
+ - {{ .IndirectExplanation }}{{ end }}
+ Locations:{{ range .Locations }}
+ - {{ .Location }}{{ end }}{{ end }}
+URLs:{{ range .URLs }}
+ - {{ . }}{{ end }}
diff --git a/grype/presenter/explain/explain_snapshot_test.go b/grype/presenter/explain/explain_snapshot_test.go
new file mode 100644
index 00000000000..4bbdd4cb211
--- /dev/null
+++ b/grype/presenter/explain/explain_snapshot_test.go
@@ -0,0 +1,71 @@
+package explain_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "os"
+ "testing"
+
+ "github.com/gkampitakis/go-snaps/snaps"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/presenter/explain"
+ "github.com/anchore/grype/grype/presenter/models"
+)
+
+func TestExplainSnapshot(t *testing.T) {
+ // load sample json
+ testCases := []struct {
+ name string
+ fixture string
+ vulnerabilityIDs []string
+ }{
+ {
+ name: "keycloak-CVE-2020-12413",
+ fixture: "./test-fixtures/keycloak-test.json",
+ vulnerabilityIDs: []string{"CVE-2020-12413"},
+ },
+ {
+ name: "chainguard-ruby-CVE-2023-28755",
+ fixture: "test-fixtures/chainguard-ruby-test.json",
+ vulnerabilityIDs: []string{"CVE-2023-28755"},
+ },
+ {
+ name: "test a GHSA",
+ /*
+ fixture created by:
+ Saving output of
+ grype anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da -o json
+ Then filtering matches to relevant ones:
+ jq -c '.matches[]' | rg -e GHSA-cfh5-3ghh-wfjx -e CVE-2014-3577 | jq -s .
+ */
+ fixture: "test-fixtures/ghsa-test.json",
+ vulnerabilityIDs: []string{"GHSA-cfh5-3ghh-wfjx"},
+ },
+ {
+ name: "test a CVE alias of a GHSA",
+ fixture: "test-fixtures/ghsa-test.json",
+ vulnerabilityIDs: []string{"CVE-2014-3577"},
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ r, err := os.Open(tc.fixture)
+ require.NoError(t, err)
+
+ // parse to models.Document
+ doc := models.Document{}
+ decoder := json.NewDecoder(r)
+ err = decoder.Decode(&doc)
+ require.NoError(t, err)
+ // create explain.VulnerabilityExplainer
+ w := bytes.NewBufferString("")
+ explainer := explain.NewVulnerabilityExplainer(w, &doc)
+ // call ExplainByID
+ err = explainer.ExplainByID(tc.vulnerabilityIDs)
+ require.NoError(t, err)
+ // assert output
+ snaps.MatchSnapshot(t, w.String())
+ })
+ }
+}
diff --git a/grype/presenter/explain/test-fixtures/chainguard-ruby-test.json b/grype/presenter/explain/test-fixtures/chainguard-ruby-test.json
new file mode 100644
index 00000000000..713d44db843
--- /dev/null
+++ b/grype/presenter/explain/test-fixtures/chainguard-ruby-test.json
@@ -0,0 +1,429 @@
+{
+ "matches": [
+ {
+ "vulnerability": {
+ "id": "CVE-2023-28755",
+ "dataSource": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755",
+ "namespace": "wolfi:distro:wolfi:rolling",
+ "severity": "High",
+ "urls": [
+ "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755"
+ ],
+ "cvss": [],
+ "fix": {
+ "versions": [
+ "3.0.6-r0"
+ ],
+ "state": "fixed"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [
+ {
+ "id": "CVE-2023-28755",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-28755",
+ "namespace": "nvd:cpe",
+ "severity": "High",
+ "urls": [
+ "https://github.com/ruby/uri/releases/",
+ "https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html",
+ "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/",
+ "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/",
+ "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/",
+ "https://www.ruby-lang.org/en/downloads/releases/",
+ "https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/",
+ "https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/"
+ ],
+ "description": "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.",
+ "cvss": [
+ {
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
+ "metrics": {
+ "baseScore": 7.5,
+ "exploitabilityScore": 3.9,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {}
+ }
+ ]
+ }
+ ],
+ "matchDetails": [
+ {
+ "type": "exact-indirect-match",
+ "matcher": "apk-matcher",
+ "searchedBy": {
+ "distro": {
+ "type": "wolfi",
+ "version": "20221118"
+ },
+ "namespace": "wolfi:distro:wolfi:rolling",
+ "package": {
+ "name": "ruby-3.0",
+ "version": "3.0.4-r1"
+ }
+ },
+ "found": {
+ "versionConstraint": "< 3.0.6-r0 (apk)",
+ "vulnerabilityID": "CVE-2023-28755"
+ }
+ },
+ {
+ "type": "exact-direct-match",
+ "matcher": "apk-matcher",
+ "searchedBy": {
+ "distro": {
+ "type": "wolfi",
+ "version": "20221118"
+ },
+ "namespace": "wolfi:distro:wolfi:rolling",
+ "package": {
+ "name": "ruby-3.0",
+ "version": "3.0.4-r1"
+ }
+ },
+ "found": {
+ "versionConstraint": "< 3.0.6-r0 (apk)",
+ "vulnerabilityID": "CVE-2023-28755"
+ }
+ }
+ ],
+ "artifact": {
+ "name": "ruby-3.0",
+ "version": "3.0.4-r1",
+ "type": "apk",
+ "locations": [
+ {
+ "path": "/lib/apk/db/installed",
+ "layerID": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880"
+ }
+ ],
+ "language": "",
+ "licenses": [
+ "PSF-2.0"
+ ],
+ "cpes": [
+ "cpe:2.3:a:ruby-lang:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby-lang:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby_lang:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby_lang:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby-3.0:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby-3.0:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby_3.0:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby_3.0:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby-lang:ruby:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby_lang:ruby:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby-3.0:ruby:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby_3.0:ruby:3.0.4-r1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby:ruby:3.0.4-r1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118",
+ "upstreams": [
+ {
+ "name": "ruby-3.0"
+ }
+ ]
+ }
+ },
+ {
+ "vulnerability": {
+ "id": "CVE-2023-28755",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-28755",
+ "namespace": "nvd:cpe",
+ "severity": "High",
+ "urls": [
+ "https://github.com/ruby/uri/releases/",
+ "https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html",
+ "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/",
+ "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/",
+ "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/",
+ "https://www.ruby-lang.org/en/downloads/releases/",
+ "https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/",
+ "https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/"
+ ],
+ "description": "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.",
+ "cvss": [
+ {
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
+ "metrics": {
+ "baseScore": 7.5,
+ "exploitabilityScore": 3.9,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {}
+ }
+ ],
+ "fix": {
+ "versions": [],
+ "state": "unknown"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [],
+ "matchDetails": [
+ {
+ "type": "cpe-match",
+ "matcher": "ruby-gem-matcher",
+ "searchedBy": {
+ "namespace": "nvd:cpe",
+ "cpes": [
+ "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*"
+ ]
+ },
+ "found": {
+ "vulnerabilityID": "CVE-2023-28755",
+ "versionConstraint": "<= 0.10.0 || = 0.10.1 || = 0.11.0 || = 0.12.0 (unknown)",
+ "cpes": [
+ "cpe:2.3:a:ruby-lang:uri:*:*:*:*:*:ruby:*:*",
+ "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:ruby:*:*"
+ ]
+ }
+ }
+ ],
+ "artifact": {
+ "name": "uri",
+ "version": "0.10.1",
+ "type": "gem",
+ "locations": [
+ {
+ "path": "/usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec",
+ "layerID": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880"
+ }
+ ],
+ "language": "ruby",
+ "licenses": [
+ "Ruby",
+ "BSD-2-Clause"
+ ],
+ "cpes": [
+ "cpe:2.3:a:akira-yamada:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:akira_yamada:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby_lang:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:uri:uri:0.10.1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:gem/uri@0.10.1",
+ "upstreams": []
+ }
+ },
+ {
+ "vulnerability": {
+ "id": "GHSA-hv5j-3h9f-99c2",
+ "dataSource": "https://github.com/advisories/GHSA-hv5j-3h9f-99c2",
+ "namespace": "github:language:ruby",
+ "severity": "High",
+ "urls": [
+ "https://github.com/advisories/GHSA-hv5j-3h9f-99c2"
+ ],
+ "description": "Ruby URI component ReDoS issue",
+ "cvss": [],
+ "fix": {
+ "versions": [
+ "0.10.2"
+ ],
+ "state": "fixed"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [
+ {
+ "id": "CVE-2023-28755",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-28755",
+ "namespace": "nvd:cpe",
+ "severity": "High",
+ "urls": [
+ "https://github.com/ruby/uri/releases/",
+ "https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html",
+ "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/",
+ "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/",
+ "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/",
+ "https://www.ruby-lang.org/en/downloads/releases/",
+ "https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/",
+ "https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/"
+ ],
+ "description": "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.",
+ "cvss": [
+ {
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
+ "metrics": {
+ "baseScore": 7.5,
+ "exploitabilityScore": 3.9,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {}
+ }
+ ]
+ }
+ ],
+ "matchDetails": [
+ {
+ "type": "exact-direct-match",
+ "matcher": "ruby-gem-matcher",
+ "searchedBy": {
+ "language": "ruby",
+ "namespace": "github:language:ruby"
+ },
+ "found": {
+ "versionConstraint": "=0.10.1 (unknown)",
+ "vulnerabilityID": "GHSA-hv5j-3h9f-99c2"
+ }
+ }
+ ],
+ "artifact": {
+ "name": "uri",
+ "version": "0.10.1",
+ "type": "gem",
+ "locations": [
+ {
+ "path": "/usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec",
+ "layerID": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880"
+ }
+ ],
+ "language": "ruby",
+ "licenses": [
+ "Ruby",
+ "BSD-2-Clause"
+ ],
+ "cpes": [
+ "cpe:2.3:a:akira-yamada:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:akira_yamada:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby_lang:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:ruby:uri:0.10.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:uri:uri:0.10.1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:gem/uri@0.10.1",
+ "upstreams": []
+ }
+ }
+ ],
+ "source": {
+ "type": "image",
+ "target": {
+ "userInput": "cgr.dev/chainguard/ruby:latest-3.0",
+ "imageID": "sha256:2f88265cfbc43ca35cd327347a9f59375b9f29ef998b8a54a882e31111266640",
+ "manifestDigest": "sha256:86abe662dfa3746038eea6b0db91092b0767d78b8b8938a343d614cd1579adc2",
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "tags": [
+ "cgr.dev/chainguard/ruby:latest-3.0"
+ ],
+ "imageSize": 38865264,
+ "layers": [
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "digest": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880",
+ "size": 38865264
+ }
+ ],
+ "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo1NTMsImRpZ2VzdCI6InNoYTI1NjoyZjg4MjY1Y2ZiYzQzY2EzNWNkMzI3MzQ3YTlmNTkzNzViOWYyOWVmOTk4YjhhNTRhODgyZTMxMTExMjY2NjQwIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjQxMTYxNzI4LCJkaWdlc3QiOiJzaGEyNTY6ZWQ5MDVmYzA2ZWQzMTc2MzE1YmQxZTMzMDc1Y2E1YjA5Y2Q3NjhhZDc4MTQyZmI0NTQzOTM1MDQ2OTU1Njg4MCJ9XX0=",
+ "config": "eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImF1dGhvciI6ImdpdGh1Yi5jb20vY2hhaW5ndWFyZC1kZXYvYXBrbyIsImNyZWF0ZWQiOiIyMDIzLTAxLTEzVDAwOjExOjI2WiIsImhpc3RvcnkiOlt7ImF1dGhvciI6ImFwa28iLCJjcmVhdGVkIjoiMjAyMy0wMS0xM1QwMDoxMToyNloiLCJjcmVhdGVkX2J5IjoiYXBrbyIsImNvbW1lbnQiOiJUaGlzIGlzIGFuIGFwa28gc2luZ2xlLWxheWVyIGltYWdlIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZWQ5MDVmYzA2ZWQzMTc2MzE1YmQxZTMzMDc1Y2E1YjA5Y2Q3NjhhZDc4MTQyZmI0NTQzOTM1MDQ2OTU1Njg4MCJdfSwiY29uZmlnIjp7IkNtZCI6WyIvdXNyL2Jpbi9pcmIiXSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIiwiU1NMX0NFUlRfRklMRT0vZXRjL3NzbC9jZXJ0cy9jYS1jZXJ0aWZpY2F0ZXMuY3J0Il0sIlVzZXIiOiI2NTUzMiIsIldvcmtpbmdEaXIiOiIvd29yayJ9fQ==",
+ "repoDigests": [
+ "cgr.dev/chainguard/ruby@sha256:3c9afb4f188827ea1062ec3b8acea32893236a0d7df31e0498df93486cff0978"
+ ],
+ "architecture": "arm64",
+ "os": "linux"
+ }
+ },
+ "distro": {
+ "name": "wolfi",
+ "version": "20221118",
+ "idLike": []
+ },
+ "descriptor": {
+ "name": "grype",
+ "version": "0.61.1",
+ "configuration": {
+ "configPath": "",
+ "verbosity": 0,
+ "output": "json",
+ "file": "",
+ "distro": "",
+ "add-cpes-if-none": false,
+ "output-template-file": "",
+ "check-for-app-update": true,
+ "only-fixed": false,
+ "only-notfixed": false,
+ "platform": "",
+ "search": {
+ "scope": "Squashed",
+ "unindexed-archives": false,
+ "indexed-archives": true
+ },
+ "ignore": null,
+ "exclude": [],
+ "db": {
+ "cache-dir": "/Users/willmurphy/Library/Caches/grype/db",
+ "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json",
+ "ca-cert": "",
+ "auto-update": true,
+ "validate-by-hash-on-start": false,
+ "validate-age": true,
+ "max-allowed-built-age": 432000000000000
+ },
+ "externalSources": {
+ "enable": false,
+ "maven": {
+ "searchUpstreamBySha1": true,
+ "baseUrl": "https://search.maven.org/solrsearch/select"
+ }
+ },
+ "match": {
+ "java": {
+ "using-cpes": true
+ },
+ "dotnet": {
+ "using-cpes": true
+ },
+ "golang": {
+ "using-cpes": true
+ },
+ "javascript": {
+ "using-cpes": false
+ },
+ "python": {
+ "using-cpes": true
+ },
+ "ruby": {
+ "using-cpes": true
+ },
+ "stock": {
+ "using-cpes": true
+ }
+ },
+ "dev": {
+ "profile-cpu": false,
+ "profile-mem": false
+ },
+ "fail-on-severity": "",
+ "registry": {
+ "insecure-skip-tls-verify": false,
+ "insecure-use-http": false,
+ "auth": []
+ },
+ "log": {
+ "quiet": false,
+ "verbosity": 0,
+ "level": "warn",
+ "file": ""
+ },
+ "show-suppressed": false,
+ "by-cve": false,
+ "name": "",
+ "default-image-pull-source": ""
+ },
+ "db": {
+ "built": "2023-05-17T01:32:43Z",
+ "schemaVersion": 5,
+ "location": "/Users/willmurphy/Library/Caches/grype/db/5",
+ "checksum": "sha256:84ebb8325f426565e7a0cd00b2ea265a0ee0ec69db158a65541a42fddd1e15b0",
+ "error": null
+ },
+ "timestamp": "2023-05-17T21:00:56.783213-04:00"
+ }
+}
diff --git a/grype/presenter/explain/test-fixtures/ghsa-test.json b/grype/presenter/explain/test-fixtures/ghsa-test.json
new file mode 100644
index 00000000000..4eaed3c137a
--- /dev/null
+++ b/grype/presenter/explain/test-fixtures/ghsa-test.json
@@ -0,0 +1,396 @@
+{
+ "matches": [
+ {
+ "vulnerability": {
+ "id": "GHSA-cfh5-3ghh-wfjx",
+ "dataSource": "https://github.com/advisories/GHSA-cfh5-3ghh-wfjx",
+ "namespace": "github:language:java",
+ "severity": "Medium",
+ "urls": [
+ "https://github.com/advisories/GHSA-cfh5-3ghh-wfjx"
+ ],
+ "description": "Moderate severity vulnerability that affects org.apache.httpcomponents:httpclient",
+ "cvss": [],
+ "fix": {
+ "versions": [
+ "4.3.5"
+ ],
+ "state": "fixed"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [
+ {
+ "id": "CVE-2014-3577",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2014-3577",
+ "namespace": "nvd:cpe",
+ "severity": "Medium",
+ "urls": [
+ "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html",
+ "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html",
+ "http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1146.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1166.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1833.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1834.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1835.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1836.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1891.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1892.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0125.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0158.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0675.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0720.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0765.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0850.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0851.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-1176.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-1177.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-1888.html",
+ "http://rhn.redhat.com/errata/RHSA-2016-1773.html",
+ "http://rhn.redhat.com/errata/RHSA-2016-1931.html",
+ "http://seclists.org/fulldisclosure/2014/Aug/48",
+ "http://secunia.com/advisories/60466",
+ "http://www.openwall.com/lists/oss-security/2021/10/06/1",
+ "http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html",
+ "http://www.osvdb.org/110143",
+ "http://www.securityfocus.com/bid/69258",
+ "http://www.securitytracker.com/id/1030812",
+ "http://www.ubuntu.com/usn/USN-2769-1",
+ "https://access.redhat.com/solutions/1165533",
+ "https://exchange.xforce.ibmcloud.com/vulnerabilities/95327",
+ "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564",
+ "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782",
+ "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E",
+ "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E",
+ "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E",
+ "https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E"
+ ],
+ "description": "org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a \"CN=\" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the \"foo,CN=www.apache.org\" string in the O field.",
+ "cvss": [
+ {
+ "source": "nvd@nist.gov",
+ "type": "Primary",
+ "version": "2.0",
+ "vector": "AV:N/AC:M/Au:N/C:P/I:P/A:N",
+ "metrics": {
+ "baseScore": 5.8,
+ "exploitabilityScore": 8.6,
+ "impactScore": 4.9
+ },
+ "vendorMetadata": {}
+ }
+ ]
+ }
+ ],
+ "matchDetails": [
+ {
+ "type": "exact-direct-match",
+ "matcher": "java-matcher",
+ "searchedBy": {
+ "language": "java",
+ "namespace": "github:language:java",
+ "package": {
+ "name": "httpclient",
+ "version": "4.1.1"
+ }
+ },
+ "found": {
+ "versionConstraint": "<4.3.5 (unknown)",
+ "vulnerabilityID": "GHSA-cfh5-3ghh-wfjx"
+ }
+ }
+ ],
+ "artifact": {
+ "id": "f09cdae46b001bc5",
+ "name": "httpclient",
+ "version": "4.1.1",
+ "type": "java-archive",
+ "locations": [
+ {
+ "path": "/TwilioNotifier.hpi",
+ "layerID": "sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9"
+ }
+ ],
+ "language": "java",
+ "licenses": [],
+ "cpes": [
+ "cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:maven/org.apache.httpcomponents/httpclient@4.1.1",
+ "upstreams": [],
+ "metadataType": "JavaMetadata",
+ "metadata": {
+ "virtualPath": "/TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient",
+ "pomArtifactID": "httpclient",
+ "pomGroupID": "org.apache.httpcomponents",
+ "manifestName": "",
+ "archiveDigests": null
+ }
+ }
+ },
+ {
+ "vulnerability": {
+ "id": "CVE-2014-3577",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2014-3577",
+ "namespace": "nvd:cpe",
+ "severity": "Medium",
+ "urls": [
+ "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html",
+ "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html",
+ "http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1146.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1166.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1833.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1834.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1835.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1836.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1891.html",
+ "http://rhn.redhat.com/errata/RHSA-2014-1892.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0125.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0158.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0675.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0720.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0765.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0850.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-0851.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-1176.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-1177.html",
+ "http://rhn.redhat.com/errata/RHSA-2015-1888.html",
+ "http://rhn.redhat.com/errata/RHSA-2016-1773.html",
+ "http://rhn.redhat.com/errata/RHSA-2016-1931.html",
+ "http://seclists.org/fulldisclosure/2014/Aug/48",
+ "http://secunia.com/advisories/60466",
+ "http://www.openwall.com/lists/oss-security/2021/10/06/1",
+ "http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html",
+ "http://www.osvdb.org/110143",
+ "http://www.securityfocus.com/bid/69258",
+ "http://www.securitytracker.com/id/1030812",
+ "http://www.ubuntu.com/usn/USN-2769-1",
+ "https://access.redhat.com/solutions/1165533",
+ "https://exchange.xforce.ibmcloud.com/vulnerabilities/95327",
+ "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564",
+ "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782",
+ "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E",
+ "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E",
+ "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E",
+ "https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E",
+ "https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E"
+ ],
+ "description": "org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a \"CN=\" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the \"foo,CN=www.apache.org\" string in the O field.",
+ "cvss": [
+ {
+ "source": "nvd@nist.gov",
+ "type": "Primary",
+ "version": "2.0",
+ "vector": "AV:N/AC:M/Au:N/C:P/I:P/A:N",
+ "metrics": {
+ "baseScore": 5.8,
+ "exploitabilityScore": 8.6,
+ "impactScore": 4.9
+ },
+ "vendorMetadata": {}
+ }
+ ],
+ "fix": {
+ "versions": [],
+ "state": "unknown"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [],
+ "matchDetails": [
+ {
+ "type": "cpe-match",
+ "matcher": "java-matcher",
+ "searchedBy": {
+ "namespace": "nvd:cpe",
+ "cpes": [
+ "cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*"
+ ],
+ "Package": {
+ "name": "httpclient",
+ "version": "4.1.1"
+ }
+ },
+ "found": {
+ "vulnerabilityID": "CVE-2014-3577",
+ "versionConstraint": ">= 4.0, <= 4.3.4 (unknown)",
+ "cpes": [
+ "cpe:2.3:a:apache:httpclient:*:*:*:*:*:*:*:*"
+ ]
+ }
+ }
+ ],
+ "artifact": {
+ "id": "f09cdae46b001bc5",
+ "name": "httpclient",
+ "version": "4.1.1",
+ "type": "java-archive",
+ "locations": [
+ {
+ "path": "/TwilioNotifier.hpi",
+ "layerID": "sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9"
+ }
+ ],
+ "language": "java",
+ "licenses": [],
+ "cpes": [
+ "cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:maven/org.apache.httpcomponents/httpclient@4.1.1",
+ "upstreams": [],
+ "metadataType": "JavaMetadata",
+ "metadata": {
+ "virtualPath": "/TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient",
+ "pomArtifactID": "httpclient",
+ "pomGroupID": "org.apache.httpcomponents",
+ "manifestName": "",
+ "archiveDigests": null
+ }
+ }
+ }
+ ],
+ "source": {
+ "type": "image",
+ "target": {
+ "userInput": "anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da",
+ "imageID": "sha256:e1a0913e5e6eb346f15791e9627842ae80b14564f9c7a4f2e0910a9433673d8b",
+ "manifestDigest": "sha256:1212e7636ec0b1a7b90eb354e761e67163c2256de127036f086876e190631b43",
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "tags": [],
+ "imageSize": 42104079,
+ "layers": [
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "digest": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68",
+ "size": 5590942
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "digest": "sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9",
+ "size": 36511427
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "digest": "sha256:5d5007f009bb615228db4046d5cae910563859d1e3a37cadb2d691ea783ad8a7",
+ "size": 1710
+ }
+ ],
+ "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoyMTU5LCJkaWdlc3QiOiJzaGEyNTY6ZTFhMDkxM2U1ZTZlYjM0NmYxNTc5MWU5NjI3ODQyYWU4MGIxNDU2NGY5YzdhNGYyZTA5MTBhOTQzMzY3M2Q4YiJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODY1NDcyLCJkaWdlc3QiOiJzaGEyNTY6ZTJlYjA2ZDhhZjgyMThjZmVjODIxMDE0NzM1N2E2OGI3ZTEzZjdjNDg1Yjk5MWMyODhjMmQwMWRjMjI4YmI2OCJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM2NTE1MzI4LCJkaWdlc3QiOiJzaGEyNTY6NmNjNmRiMTc2NDQwZTNkYzMyMThkMmUzMjU3MTZjMTkyMmVhOWQ5MDBiNjFkN2FkNmYzODhmZDBlZDJiNGVmOSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM1ODQsImRpZ2VzdCI6InNoYTI1Njo1ZDUwMDdmMDA5YmI2MTUyMjhkYjQwNDZkNWNhZTkxMDU2Mzg1OWQxZTNhMzdjYWRiMmQ2OTFlYTc4M2FkOGE3In1dfQ==",
+ "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiSW1hZ2UiOiJzaGEyNTY6YTUyNzg0NzAxODkzMmE0ZWZlMWYxM2U4MzY3NTE4YzQ0MmI2MzE1OTA3YTE2MDRiZWJhYTJhZjg1NjgwMTc1MSIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOm51bGx9LCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiJdLCJDbWQiOlsiL2Jpbi9zaCIsIi1jIiwiIyhub3ApIENPUFkgZmlsZTo4ZTY2YzA3MmFjYjU4ZTVjN2ViZWU5MGI0ZGVhNjc1YjdjM2VmOTA5MTQ0Yjk3MzA4MzYwMGU3N2NkNDIyNzY5IGluIC8gIl0sIkltYWdlIjoic2hhMjU2OmE1Mjc4NDcwMTg5MzJhNGVmZTFmMTNlODM2NzUxOGM0NDJiNjMxNTkwN2ExNjA0YmViYWEyYWY4NTY4MDE3NTEiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMjJUMTc6MDY6MzIuOTIxOTkxNjI5WiIsImRvY2tlcl92ZXJzaW9uIjoiMjAuMTAuNyIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA4LTI3VDE3OjE5OjQ1LjU1MzA5MjM2M1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YWFkNDI5MGQyNzU4MGNjMWEwOTRmZmFmOThjM2NhMmZjNWQ2OTlmZTY5NWRmYjhlNmU5ZmFjMjBmMTEyOTQ1MCBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjEtMDgtMjdUMTc6MTk6NDUuNzU4NjExNTIzWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcIi9iaW4vc2hcIl0iLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMS0xMC0yMlQxNzowNjozMi43MTI3NTA5MjRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgd2dldCAtbnYgaHR0cHM6Ly9yZXBvMS5tYXZlbi5vcmcvbWF2ZW4yL2p1bml0L2p1bml0LzQuMTMuMS9qdW5pdC00LjEzLjEuamFyIFx1MDAyNlx1MDAyNiAgICAgd2dldCAtbnYgaHR0cHM6Ly9nZXQuamVua2lucy5pby9wbHVnaW5zL1R3aWxpb05vdGlmaWVyLzAuMi4xL1R3aWxpb05vdGlmaWVyLmhwaSBcdTAwMjZcdTAwMjYgICAgIHdnZXQgLW52IGh0dHBzOi8vdXBkYXRlcy5qZW5raW5zLWNpLm9yZy9kb3dubG9hZC93YXIvMS4zOTAvaHVkc29uLndhciBcdTAwMjZcdTAwMjYgICAgIHdnZXQgLW52IGh0dHBzOi8vZ2V0LmplbmtpbnMuaW8vcGx1Z2lucy9ub21hZC8wLjcuNC9ub21hZC5ocGkifSx7ImNyZWF0ZWQiOiIyMDIxLTEwLTIyVDE3OjA2OjMyLjkyMTk5MTYyOVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBmaWxlOjhlNjZjMDcyYWNiNThlNWM3ZWJlZTkwYjRkZWE2NzViN2MzZWY5MDkxNDRiOTczMDgzNjAwZTc3Y2Q0MjI3NjkgaW4gLyAifV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1NjplMmViMDZkOGFmODIxOGNmZWM4MjEwMTQ3MzU3YTY4YjdlMTNmN2M0ODViOTkxYzI4OGMyZDAxZGMyMjhiYjY4Iiwic2hhMjU2OjZjYzZkYjE3NjQ0MGUzZGMzMjE4ZDJlMzI1NzE2YzE5MjJlYTlkOTAwYjYxZDdhZDZmMzg4ZmQwZWQyYjRlZjkiLCJzaGEyNTY6NWQ1MDA3ZjAwOWJiNjE1MjI4ZGI0MDQ2ZDVjYWU5MTA1NjM4NTlkMWUzYTM3Y2FkYjJkNjkxZWE3ODNhZDhhNyJdfX0=",
+ "repoDigests": [
+ "anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da"
+ ],
+ "architecture": "amd64",
+ "os": "linux"
+ }
+ },
+ "distro": {
+ "name": "alpine",
+ "version": "3.14.2",
+ "idLike": []
+ },
+ "descriptor": {
+ "name": "grype",
+ "version": "0.65.1",
+ "configuration": {
+ "configPath": "",
+ "verbosity": 0,
+ "output": [
+ "json"
+ ],
+ "file": "",
+ "distro": "",
+ "add-cpes-if-none": false,
+ "output-template-file": "",
+ "check-for-app-update": true,
+ "only-fixed": false,
+ "only-notfixed": false,
+ "platform": "",
+ "search": {
+ "scope": "Squashed",
+ "unindexed-archives": false,
+ "indexed-archives": true
+ },
+ "ignore": null,
+ "exclude": [],
+ "db": {
+ "cache-dir": "/Users/willmurphy/Library/Caches/grype/db",
+ "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json",
+ "ca-cert": "",
+ "auto-update": true,
+ "validate-by-hash-on-start": false,
+ "validate-age": true,
+ "max-allowed-built-age": 432000000000000
+ },
+ "externalSources": {
+ "enable": false,
+ "maven": {
+ "searchUpstreamBySha1": true,
+ "baseUrl": "https://search.maven.org/solrsearch/select"
+ }
+ },
+ "match": {
+ "java": {
+ "using-cpes": true
+ },
+ "dotnet": {
+ "using-cpes": true
+ },
+ "golang": {
+ "using-cpes": true
+ },
+ "javascript": {
+ "using-cpes": false
+ },
+ "python": {
+ "using-cpes": true
+ },
+ "ruby": {
+ "using-cpes": true
+ },
+ "stock": {
+ "using-cpes": true
+ }
+ },
+ "dev": {
+ "profile-cpu": false,
+ "profile-mem": false
+ },
+ "fail-on-severity": "",
+ "registry": {
+ "insecure-skip-tls-verify": false,
+ "insecure-use-http": false,
+ "auth": []
+ },
+ "log": {
+ "quiet": true,
+ "verbosity": 0,
+ "level": "",
+ "file": ""
+ },
+ "show-suppressed": false,
+ "by-cve": false,
+ "name": "",
+ "default-image-pull-source": ""
+ },
+ "db": {
+ "built": "2023-08-31T01:24:19Z",
+ "schemaVersion": 5,
+ "location": "/Users/willmurphy/Library/Caches/grype/db/5",
+ "checksum": "sha256:911c05ea7c2a5f993758e5428c614914384c2a8265d7e2b0edb843799d62626c",
+ "error": null
+ },
+ "timestamp": "2023-08-31T15:13:32.377177-04:00"
+ }
+}
diff --git a/grype/presenter/explain/test-fixtures/keycloak-test.json b/grype/presenter/explain/test-fixtures/keycloak-test.json
new file mode 100644
index 00000000000..55edfd0231d
--- /dev/null
+++ b/grype/presenter/explain/test-fixtures/keycloak-test.json
@@ -0,0 +1,839 @@
+{
+ "matches": [{
+ "vulnerability": {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413",
+ "namespace": "redhat:distro:redhat:9",
+ "severity": "Low",
+ "urls": [
+ "https://access.redhat.com/security/cve/CVE-2020-12413"
+ ],
+ "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.",
+ "cvss": [
+ {
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {
+ "base_severity": "Medium",
+ "status": "draft"
+ }
+ }
+ ],
+ "fix": {
+ "versions": [],
+ "state": "wont-fix"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [
+ {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413",
+ "namespace": "nvd:cpe",
+ "severity": "Medium",
+ "urls": [
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413",
+ "https://raccoon-attack.com/"
+ ],
+ "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.",
+ "cvss": [
+ {
+ "source": "nvd@nist.gov",
+ "type": "Primary",
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {}
+ }
+ ]
+ }
+ ],
+ "matchDetails": [
+ {
+ "type": "exact-indirect-match",
+ "matcher": "rpm-matcher",
+ "searchedBy": {
+ "distro": {
+ "type": "redhat",
+ "version": "9.1"
+ },
+ "namespace": "redhat:distro:redhat:9",
+ "package": {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ },
+ "found": {
+ "versionConstraint": "none (rpm)",
+ "vulnerabilityID": "CVE-2020-12413"
+ }
+ }
+ ],
+ "artifact": {
+ "id": "ff2aefb138ebd4bf",
+ "name": "nspr",
+ "version": "4.34.0-17.el9_1",
+ "type": "rpm",
+ "locations": [
+ {
+ "path": "/var/lib/rpm/rpmdb.sqlite",
+ "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b"
+ }
+ ],
+ "language": "",
+ "licenses": [
+ "MPLv2.0"
+ ],
+ "cpes": [
+ "cpe:2.3:a:redhat:nspr:4.34.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nspr:nspr:4.34.0-17.el9_1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1",
+ "upstreams": [
+ {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ ],
+ "metadataType": "RpmMetadata",
+ "metadata": {
+ "epoch": null,
+ "modularityLabel": ""
+ }
+ }
+ },
+ {
+ "vulnerability": {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413",
+ "namespace": "redhat:distro:redhat:9",
+ "severity": "Low",
+ "urls": [
+ "https://access.redhat.com/security/cve/CVE-2020-12413"
+ ],
+ "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.",
+ "cvss": [
+ {
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {
+ "base_severity": "Medium",
+ "status": "draft"
+ }
+ }
+ ],
+ "fix": {
+ "versions": [],
+ "state": "wont-fix"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [
+ {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413",
+ "namespace": "nvd:cpe",
+ "severity": "Medium",
+ "urls": [
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413",
+ "https://raccoon-attack.com/"
+ ],
+ "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.",
+ "cvss": [
+ {
+ "source": "nvd@nist.gov",
+ "type": "Primary",
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {}
+ }
+ ]
+ }
+ ],
+ "matchDetails": [
+ {
+ "type": "exact-direct-match",
+ "matcher": "rpm-matcher",
+ "searchedBy": {
+ "distro": {
+ "type": "redhat",
+ "version": "9.1"
+ },
+ "namespace": "redhat:distro:redhat:9",
+ "package": {
+ "name": "nss",
+ "version": "0:3.79.0-17.el9_1"
+ }
+ },
+ "found": {
+ "versionConstraint": "none (rpm)",
+ "vulnerabilityID": "CVE-2020-12413"
+ }
+ }
+ ],
+ "artifact": {
+ "id": "840f8a931c86688f",
+ "name": "nss",
+ "version": "3.79.0-17.el9_1",
+ "type": "rpm",
+ "locations": [
+ {
+ "path": "/var/lib/rpm/rpmdb.sqlite",
+ "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b"
+ }
+ ],
+ "language": "",
+ "licenses": [
+ "MPLv2.0"
+ ],
+ "cpes": [
+ "cpe:2.3:a:redhat:nss:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss:nss:3.79.0-17.el9_1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1",
+ "upstreams": [],
+ "metadataType": "RpmMetadata",
+ "metadata": {
+ "epoch": null,
+ "modularityLabel": ""
+ }
+ }
+ },
+ {
+ "vulnerability": {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413",
+ "namespace": "redhat:distro:redhat:9",
+ "severity": "Low",
+ "urls": [
+ "https://access.redhat.com/security/cve/CVE-2020-12413"
+ ],
+ "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.",
+ "cvss": [
+ {
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {
+ "base_severity": "Medium",
+ "status": "draft"
+ }
+ }
+ ],
+ "fix": {
+ "versions": [],
+ "state": "wont-fix"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [
+ {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413",
+ "namespace": "nvd:cpe",
+ "severity": "Medium",
+ "urls": [
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413",
+ "https://raccoon-attack.com/"
+ ],
+ "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.",
+ "cvss": [
+ {
+ "source": "nvd@nist.gov",
+ "type": "Primary",
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {}
+ }
+ ]
+ }
+ ],
+ "matchDetails": [
+ {
+ "type": "exact-indirect-match",
+ "matcher": "rpm-matcher",
+ "searchedBy": {
+ "distro": {
+ "type": "redhat",
+ "version": "9.1"
+ },
+ "namespace": "redhat:distro:redhat:9",
+ "package": {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ },
+ "found": {
+ "versionConstraint": "none (rpm)",
+ "vulnerabilityID": "CVE-2020-12413"
+ }
+ }
+ ],
+ "artifact": {
+ "id": "7d1c659d9eb00024",
+ "name": "nss-softokn",
+ "version": "3.79.0-17.el9_1",
+ "type": "rpm",
+ "locations": [
+ {
+ "path": "/var/lib/rpm/rpmdb.sqlite",
+ "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b"
+ }
+ ],
+ "language": "",
+ "licenses": [
+ "MPLv2.0"
+ ],
+ "cpes": [
+ "cpe:2.3:a:nss-softokn:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss-softokn:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_softokn:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_softokn:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:redhat:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:redhat:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1",
+ "upstreams": [
+ {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ ],
+ "metadataType": "RpmMetadata",
+ "metadata": {
+ "epoch": null,
+ "modularityLabel": ""
+ }
+ }
+ },
+ {
+ "vulnerability": {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413",
+ "namespace": "redhat:distro:redhat:9",
+ "severity": "Low",
+ "urls": [
+ "https://access.redhat.com/security/cve/CVE-2020-12413"
+ ],
+ "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.",
+ "cvss": [
+ {
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {
+ "base_severity": "Medium",
+ "status": "draft"
+ }
+ }
+ ],
+ "fix": {
+ "versions": [],
+ "state": "wont-fix"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [
+ {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413",
+ "namespace": "nvd:cpe",
+ "severity": "Medium",
+ "urls": [
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413",
+ "https://raccoon-attack.com/"
+ ],
+ "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.",
+ "cvss": [
+ {
+ "source": "nvd@nist.gov",
+ "type": "Primary",
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {}
+ }
+ ]
+ }
+ ],
+ "matchDetails": [
+ {
+ "type": "exact-indirect-match",
+ "matcher": "rpm-matcher",
+ "searchedBy": {
+ "distro": {
+ "type": "redhat",
+ "version": "9.1"
+ },
+ "namespace": "redhat:distro:redhat:9",
+ "package": {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ },
+ "found": {
+ "versionConstraint": "none (rpm)",
+ "vulnerabilityID": "CVE-2020-12413"
+ }
+ }
+ ],
+ "artifact": {
+ "id": "cb1f96627e29924e",
+ "name": "nss-softokn-freebl",
+ "version": "3.79.0-17.el9_1",
+ "type": "rpm",
+ "locations": [
+ {
+ "path": "/var/lib/rpm/rpmdb.sqlite",
+ "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b"
+ }
+ ],
+ "language": "",
+ "licenses": [
+ "MPLv2.0"
+ ],
+ "cpes": [
+ "cpe:2.3:a:nss-softokn-freebl:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss-softokn-freebl:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_softokn_freebl:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_softokn_freebl:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss-softokn:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss-softokn:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_softokn:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_softokn:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:redhat:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:redhat:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:rpm/rhel/nss-softokn-freebl@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1",
+ "upstreams": [
+ {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ ],
+ "metadataType": "RpmMetadata",
+ "metadata": {
+ "epoch": null,
+ "modularityLabel": ""
+ }
+ }
+ },
+ {
+ "vulnerability": {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413",
+ "namespace": "redhat:distro:redhat:9",
+ "severity": "Low",
+ "urls": [
+ "https://access.redhat.com/security/cve/CVE-2020-12413"
+ ],
+ "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.",
+ "cvss": [
+ {
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {
+ "base_severity": "Medium",
+ "status": "draft"
+ }
+ }
+ ],
+ "fix": {
+ "versions": [],
+ "state": "wont-fix"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [
+ {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413",
+ "namespace": "nvd:cpe",
+ "severity": "Medium",
+ "urls": [
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413",
+ "https://raccoon-attack.com/"
+ ],
+ "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.",
+ "cvss": [
+ {
+ "source": "nvd@nist.gov",
+ "type": "Primary",
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {}
+ }
+ ]
+ }
+ ],
+ "matchDetails": [
+ {
+ "type": "exact-indirect-match",
+ "matcher": "rpm-matcher",
+ "searchedBy": {
+ "distro": {
+ "type": "redhat",
+ "version": "9.1"
+ },
+ "namespace": "redhat:distro:redhat:9",
+ "package": {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ },
+ "found": {
+ "versionConstraint": "none (rpm)",
+ "vulnerabilityID": "CVE-2020-12413"
+ }
+ }
+ ],
+ "artifact": {
+ "id": "d096d490e4fccf36",
+ "name": "nss-sysinit",
+ "version": "3.79.0-17.el9_1",
+ "type": "rpm",
+ "locations": [
+ {
+ "path": "/var/lib/rpm/rpmdb.sqlite",
+ "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b"
+ }
+ ],
+ "language": "",
+ "licenses": [
+ "MPLv2.0"
+ ],
+ "cpes": [
+ "cpe:2.3:a:nss-sysinit:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss-sysinit:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_sysinit:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_sysinit:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:redhat:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:redhat:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:rpm/rhel/nss-sysinit@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1",
+ "upstreams": [
+ {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ ],
+ "metadataType": "RpmMetadata",
+ "metadata": {
+ "epoch": null,
+ "modularityLabel": ""
+ }
+ }
+ },
+ {
+ "vulnerability": {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413",
+ "namespace": "redhat:distro:redhat:9",
+ "severity": "Low",
+ "urls": [
+ "https://access.redhat.com/security/cve/CVE-2020-12413"
+ ],
+ "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.",
+ "cvss": [
+ {
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {
+ "base_severity": "Medium",
+ "status": "draft"
+ }
+ }
+ ],
+ "fix": {
+ "versions": [],
+ "state": "wont-fix"
+ },
+ "advisories": []
+ },
+ "relatedVulnerabilities": [
+ {
+ "id": "CVE-2020-12413",
+ "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413",
+ "namespace": "nvd:cpe",
+ "severity": "Medium",
+ "urls": [
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413",
+ "https://raccoon-attack.com/"
+ ],
+ "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.",
+ "cvss": [
+ {
+ "source": "nvd@nist.gov",
+ "type": "Primary",
+ "version": "3.1",
+ "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
+ "metrics": {
+ "baseScore": 5.9,
+ "exploitabilityScore": 2.2,
+ "impactScore": 3.6
+ },
+ "vendorMetadata": {}
+ }
+ ]
+ }
+ ],
+ "matchDetails": [
+ {
+ "type": "exact-indirect-match",
+ "matcher": "rpm-matcher",
+ "searchedBy": {
+ "distro": {
+ "type": "redhat",
+ "version": "9.1"
+ },
+ "namespace": "redhat:distro:redhat:9",
+ "package": {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ },
+ "found": {
+ "versionConstraint": "none (rpm)",
+ "vulnerabilityID": "CVE-2020-12413"
+ }
+ }
+ ],
+ "artifact": {
+ "id": "641950c22b3f5035",
+ "name": "nss-util",
+ "version": "3.79.0-17.el9_1",
+ "type": "rpm",
+ "locations": [
+ {
+ "path": "/var/lib/rpm/rpmdb.sqlite",
+ "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b"
+ }
+ ],
+ "language": "",
+ "licenses": [
+ "MPLv2.0"
+ ],
+ "cpes": [
+ "cpe:2.3:a:nss-util:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss-util:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_util:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss_util:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:redhat:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:redhat:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:nss:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*"
+ ],
+ "purl": "pkg:rpm/rhel/nss-util@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1",
+ "upstreams": [
+ {
+ "name": "nss",
+ "version": "3.79.0-17.el9_1"
+ }
+ ],
+ "metadataType": "RpmMetadata",
+ "metadata": {
+ "epoch": null,
+ "modularityLabel": ""
+ }
+ }
+ }
+ ],
+ "source": {
+ "type": "image",
+ "target": {
+ "userInput": "docker.io/keycloak/keycloak:21.0.2@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50",
+ "imageID": "sha256:8cf8fd2be2ded92962d52adff75ad06a4c30f69c66facbdf223364c6c9e33b8c",
+ "manifestDigest": "sha256:d1630d3eb8285a978301bcefc5b223e564ae300750af2fc9ea3f413c5376a47e",
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "tags": [],
+ "imageSize": 433836339,
+ "layers": [
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "digest": "sha256:4e37aeaccb4c8016e381d6e2b2a0f22ea59985a7b9b8eca674726e8c60f2f51d",
+ "size": 24302817
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "digest": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b",
+ "size": 222622091
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "digest": "sha256:8329e422b4fd63ffd06518346e5f1f7b33e8190a79e5c321f9c50aba8651d30c",
+ "size": 186910556
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "digest": "sha256:987fd030ab2cd62944cb487df846c312b64dbb2a6a3131a81253c15a9da2a26c",
+ "size": 875
+ }
+ ],
+ "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NTY5LCJkaWdlc3QiOiJzaGEyNTY6OGNmOGZkMmJlMmRlZDkyOTYyZDUyYWRmZjc1YWQwNmE0YzMwZjY5YzY2ZmFjYmRmMjIzMzY0YzZjOWUzM2I4YyJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyNjEyNzg3MiwiZGlnZXN0Ijoic2hhMjU2OjRlMzdhZWFjY2I0YzgwMTZlMzgxZDZlMmIyYTBmMjJlYTU5OTg1YTdiOWI4ZWNhNjc0NzI2ZThjNjBmMmY1MWQifSx7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMjUxMTI1NzYsImRpZ2VzdCI6InNoYTI1Njo3OThkOTFmODk4NThlNjM2MjdhOThkM2ExOTZjOWVlNGQwODk5MjU5YzBmNjRiNjhiMWUwMjYwYTY3YzljZDJiIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MTg3MzE1MjAwLCJkaWdlc3QiOiJzaGEyNTY6ODMyOWU0MjJiNGZkNjNmZmQwNjUxODM0NmU1ZjFmN2IzM2U4MTkwYTc5ZTVjMzIxZjljNTBhYmE4NjUxZDMwYyJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjQwOTYsImRpZ2VzdCI6InNoYTI1Njo5ODdmZDAzMGFiMmNkNjI5NDRjYjQ4N2RmODQ2YzMxMmI2NGRiYjJhNmEzMTMxYTgxMjUzYzE1YTlkYTJhMjZjIn1dfQ==",
+ "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJVc2VyIjoiMTAwMCIsIkV4cG9zZWRQb3J0cyI6eyI4MDgwL3RjcCI6e30sIjg0NDMvdGNwIjp7fX0sIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIkxBTkc9ZW5fVVMuVVRGLTgiXSwiRW50cnlwb2ludCI6WyIvb3B0L2tleWNsb2FrL2Jpbi9rYy5zaCJdLCJMYWJlbHMiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiYnVpbGQtZGF0ZSI6IjIwMjMtMDItMjJUMTM6NTQ6MjUiLCJjb20ucmVkaGF0LmNvbXBvbmVudCI6InViaTktbWljcm8tY29udGFpbmVyIiwiY29tLnJlZGhhdC5saWNlbnNlX3Rlcm1zIjoiaHR0cHM6Ly93d3cucmVkaGF0LmNvbS9lbi9hYm91dC9yZWQtaGF0LWVuZC11c2VyLWxpY2Vuc2UtYWdyZWVtZW50cyNVQkkiLCJkZXNjcmlwdGlvbiI6IlZlcnkgc21hbGwgaW1hZ2Ugd2hpY2ggZG9lc24ndCBpbnN0YWxsIHRoZSBwYWNrYWdlIG1hbmFnZXIuIiwiZGlzdHJpYnV0aW9uLXNjb3BlIjoicHVibGljIiwiaW8uYnVpbGRhaC52ZXJzaW9uIjoiMS4yNy4zIiwiaW8uazhzLmRlc2NyaXB0aW9uIjoiVmVyeSBzbWFsbCBpbWFnZSB3aGljaCBkb2Vzbid0IGluc3RhbGwgdGhlIHBhY2thZ2UgbWFuYWdlci4iLCJpby5rOHMuZGlzcGxheS1uYW1lIjoiVWJpOS1taWNybyIsImlvLm9wZW5zaGlmdC5leHBvc2Utc2VydmljZXMiOiIiLCJtYWludGFpbmVyIjoiUmVkIEhhdCwgSW5jLiIsIm5hbWUiOiJ1Ymk5L3ViaS1taWNybyIsIm9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5jcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMjoxOC45ODVaIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uIjoiIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzIjoiQXBhY2hlLTIuMCIsIm9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5yZXZpc2lvbiI6ImIzNTJhMWY2ZThiYTkyYTA0NWI1OWNjOGRlZDE4NWUzYjFkMjYxNTUiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlIjoiaHR0cHM6Ly9naXRodWIuY29tL2tleWNsb2FrLXJlbC9rZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGUiOiJrZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL2tleWNsb2FrLXJlbC9rZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVyc2lvbiI6IjIxLjAuMiIsInJlbGVhc2UiOiIxNSIsInN1bW1hcnkiOiJ1Ymk5IG1pY3JvIGltYWdlIiwidXJsIjoiaHR0cHM6Ly9hY2Nlc3MucmVkaGF0LmNvbS9jb250YWluZXJzLyMvcmVnaXN0cnkuYWNjZXNzLnJlZGhhdC5jb20vdWJpOS91YmktbWljcm8vaW1hZ2VzLzkuMS4wLTE1IiwidmNzLXJlZiI6ImM1NjNlMDkxZTBjN2JkNWE2OWIyYTQ2OTkwZGRhNGY1OTU5NWFhMzciLCJ2Y3MtdHlwZSI6ImdpdCIsInZlbmRvciI6IlJlZCBIYXQsIEluYy4iLCJ2ZXJzaW9uIjoiOS4xLjAifSwiT25CdWlsZCI6bnVsbH0sImNyZWF0ZWQiOiIyMDIzLTAzLTMwVDExOjEzOjE0LjYyOTk2NjA5NFoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc2NTUxODNaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIG1haW50YWluZXI9XCJSZWQgSGF0LCBJbmMuXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc3ODIxNDlaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGNvbS5yZWRoYXQuY29tcG9uZW50PVwidWJpOS1taWNyby1jb250YWluZXJcIiIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjAzLjkzNzgwODc4M1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgTEFCRUwgbmFtZT1cInViaTkvdWJpLW1pY3JvXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc4NTM2MzVaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIHZlcnNpb249XCI5LjEuMFwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM3OTMwMjE1WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBjb20ucmVkaGF0LmxpY2Vuc2VfdGVybXM9XCJodHRwczovL3d3dy5yZWRoYXQuY29tL2VuL2Fib3V0L3JlZC1oYXQtZW5kLXVzZXItbGljZW5zZS1hZ3JlZW1lbnRzI1VCSVwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM3OTY5MTY5WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBzdW1tYXJ5PVwidWJpOSBtaWNybyBpbWFnZVwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM4MDAzNDAyWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBkZXNjcmlwdGlvbj1cIlZlcnkgc21hbGwgaW1hZ2Ugd2hpY2ggZG9lc24ndCBpbnN0YWxsIHRoZSBwYWNrYWdlIG1hbmFnZXIuXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45MzgyMjU4ODRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGlvLms4cy5kaXNwbGF5LW5hbWU9XCJVYmk5LW1pY3JvXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45MzgzMjAxOTlaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGlvLm9wZW5zaGlmdC5leHBvc2Utc2VydmljZXM9XCJcIiIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA0LjkxOTA1OTE2OFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBkaXI6MWMzNjc0ODA2NTg5ZWRhYmNiNTg1NGIxOThhMjlkZGU3ODZhYmI3MmU4YTU5NTA4OGRmNTI3Mjg1MzVjMTk5ZiBpbiAvICIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA1LjE4OTA4MTIyM1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBmaWxlOmVlYzczZjg1OWM2ZTdmNmM4YTk0MjdlY2M1MjQ5NTA0ZmU4OWFlNTRkYzNhMTUyMWI0NDI2NzRhOTA0OTdkMzIgaW4gL2V0Yy95dW0ucmVwb3MuZC91YmkucmVwbyAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowNS4xODkxMTM0MDRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIENNRCAvYmluL3NoIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDUuMTg5MTkyNDZaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIHJlbGVhc2U9MTUiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowNS40NTY0OTQ0MjRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOjI0ODgwMDQ4ZTQ5YzMwZDJlNWQ2ZGQ3M2VmNzg5NGU2OTIyMTczN2M5MmM0YWNjNDgxYWMxNmY1NDZjZWE3OTEgaW4gL3Jvb3QvYnVpbGRpbmZvL2NvbnRlbnRfbWFuaWZlc3RzL3ViaTktbWljcm8tY29udGFpbmVyLTkuMS4wLTE1Lmpzb24gIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDUuNzM0NTA1MjUyWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTpjYTYxZjYyNzUwMTQ4MWFlZjYyNzcwZjM4ZmY0NjQyNzY2MzcwZDBiYjE2ZjM0MzBlZjI4NzkyNzcxMDAwYjE0IGluIC9yb290L2J1aWxkaW5mby9Eb2NrZXJmaWxlLXViaTktdWJpLW1pY3JvLTkuMS4wLTE1ICIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA1Ljk3NjcyMjE4OFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgTEFCRUwgXCJkaXN0cmlidXRpb24tc2NvcGVcIj1cInB1YmxpY1wiIFwidmVuZG9yXCI9XCJSZWQgSGF0LCBJbmMuXCIgXCJidWlsZC1kYXRlXCI9XCIyMDIzLTAyLTIyVDEzOjU0OjI1XCIgXCJhcmNoaXRlY3R1cmVcIj1cIng4Nl82NFwiIFwidmNzLXR5cGVcIj1cImdpdFwiIFwidmNzLXJlZlwiPVwiYzU2M2UwOTFlMGM3YmQ1YTY5YjJhNDY5OTBkZGE0ZjU5NTk1YWEzN1wiIFwiaW8uazhzLmRlc2NyaXB0aW9uXCI9XCJWZXJ5IHNtYWxsIGltYWdlIHdoaWNoIGRvZXNuJ3QgaW5zdGFsbCB0aGUgcGFja2FnZSBtYW5hZ2VyLlwiIFwidXJsXCI9XCJodHRwczovL2FjY2Vzcy5yZWRoYXQuY29tL2NvbnRhaW5lcnMvIy9yZWdpc3RyeS5hY2Nlc3MucmVkaGF0LmNvbS91Ymk5L3ViaS1taWNyby9pbWFnZXMvOS4xLjAtMTVcIiJ9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTMuNjYwNjQ3MjY5WiIsImNyZWF0ZWRfYnkiOiJFTlYgTEFORz1lbl9VUy5VVEYtOCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTMuNjYwNjQ3MjY5WiIsImNyZWF0ZWRfYnkiOiJDT1BZIC90bXAvbnVsbC9yb290ZnMvIC8gIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC41NDg0MDQ5NzZaIiwiY3JlYXRlZF9ieSI6IkNPUFkgL29wdC9rZXljbG9hayAvb3B0L2tleWNsb2FrICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTQuNjI5OTY2MDk0WiIsImNyZWF0ZWRfYnkiOiJSVU4gL2Jpbi9zaCAtYyBlY2hvIFwia2V5Y2xvYWs6eDowOnJvb3RcIiBcdTAwM2VcdTAwM2UgL2V0Yy9ncm91cCBcdTAwMjZcdTAwMjYgICAgIGVjaG8gXCJrZXljbG9hazp4OjEwMDA6MDprZXljbG9hayB1c2VyOi9vcHQva2V5Y2xvYWs6L3NiaW4vbm9sb2dpblwiIFx1MDAzZVx1MDAzZSAvZXRjL3Bhc3N3ZCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifSx7ImNyZWF0ZWQiOiIyMDIzLTAzLTMwVDExOjEzOjE0LjYyOTk2NjA5NFoiLCJjcmVhdGVkX2J5IjoiVVNFUiAxMDAwIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVYUE9TRSBtYXBbODA4MC90Y3A6e31dIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVYUE9TRSBtYXBbODQ0My90Y3A6e31dIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVOVFJZUE9JTlQgW1wiL29wdC9rZXljbG9hay9iaW4va2Muc2hcIl0iLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCIsImVtcHR5X2xheWVyIjp0cnVlfV0sIm1vYnkuYnVpbGRraXQuYnVpbGRpbmZvLnYxIjoiZXlKbWNtOXVkR1Z1WkNJNkltUnZZMnRsY21acGJHVXVkakFpTENKemIzVnlZMlZ6SWpwYmV5SjBlWEJsSWpvaVpHOWphMlZ5TFdsdFlXZGxJaXdpY21WbUlqb2ljbVZuYVhOMGNua3VZV05qWlhOekxuSmxaR2hoZEM1amIyMHZkV0pwT1MxdGFXTnlienBzWVhSbGMzUWlMQ0p3YVc0aU9pSnphR0V5TlRZNk5UTTJOemszTVRRNVlqSmxOakUwWXpFMll6RTRaRE00WVdVM1pqVXdPRGc1TWprM1pUa3lZVGRqWXpSaE5qQXlORGt4TkRjM1pHUmpNMkppTURZeFppSjlMSHNpZEhsd1pTSTZJbVJ2WTJ0bGNpMXBiV0ZuWlNJc0luSmxaaUk2SW5KbFoybHpkSEo1TG1GalkyVnpjeTV5WldSb1lYUXVZMjl0TDNWaWFUazZiR0YwWlhOMElpd2ljR2x1SWpvaWMyaGhNalUyT21OaU16QXpOREEwWlRVM05tWm1OVFV5T0dRMFpqQTRZakV5WVdRNE5XWmhZamhtTmpGbVlUbGxOV1JpWVRZM1lqTTNZakV4T1dSaU1qUTROalZrWmpNaWZWMTkiLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6NGUzN2FlYWNjYjRjODAxNmUzODFkNmUyYjJhMGYyMmVhNTk5ODVhN2I5YjhlY2E2NzQ3MjZlOGM2MGYyZjUxZCIsInNoYTI1Njo3OThkOTFmODk4NThlNjM2MjdhOThkM2ExOTZjOWVlNGQwODk5MjU5YzBmNjRiNjhiMWUwMjYwYTY3YzljZDJiIiwic2hhMjU2OjgzMjllNDIyYjRmZDYzZmZkMDY1MTgzNDZlNWYxZjdiMzNlODE5MGE3OWU1YzMyMWY5YzUwYWJhODY1MWQzMGMiLCJzaGEyNTY6OTg3ZmQwMzBhYjJjZDYyOTQ0Y2I0ODdkZjg0NmMzMTJiNjRkYmIyYTZhMzEzMWE4MTI1M2MxNWE5ZGEyYTI2YyJdfX0=",
+ "repoDigests": [
+ "keycloak/keycloak@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50"
+ ],
+ "architecture": "amd64",
+ "os": "linux"
+ }
+ },
+ "distro": {
+ "name": "redhat",
+ "version": "9.1",
+ "idLike": [
+ "fedora"
+ ]
+ },
+ "descriptor": {
+ "name": "grype",
+ "version": "0.66.0",
+ "configuration": {
+ "configPath": "",
+ "verbosity": 0,
+ "output": [
+ "json"
+ ],
+ "file": "",
+ "distro": "",
+ "add-cpes-if-none": false,
+ "output-template-file": "",
+ "check-for-app-update": true,
+ "only-fixed": false,
+ "only-notfixed": false,
+ "platform": "",
+ "search": {
+ "scope": "Squashed",
+ "unindexed-archives": false,
+ "indexed-archives": true
+ },
+ "ignore": null,
+ "exclude": [],
+ "db": {
+ "cache-dir": "/Users/willmurphy/Library/Caches/grype/db",
+ "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json",
+ "ca-cert": "",
+ "auto-update": true,
+ "validate-by-hash-on-start": false,
+ "validate-age": true,
+ "max-allowed-built-age": 432000000000000
+ },
+ "externalSources": {
+ "enable": false,
+ "maven": {
+ "searchUpstreamBySha1": true,
+ "baseUrl": "https://search.maven.org/solrsearch/select"
+ }
+ },
+ "match": {
+ "java": {
+ "using-cpes": true
+ },
+ "dotnet": {
+ "using-cpes": true
+ },
+ "golang": {
+ "using-cpes": true
+ },
+ "javascript": {
+ "using-cpes": false
+ },
+ "python": {
+ "using-cpes": true
+ },
+ "ruby": {
+ "using-cpes": true
+ },
+ "stock": {
+ "using-cpes": true
+ }
+ },
+ "dev": {
+ "profile-cpu": false,
+ "profile-mem": false
+ },
+ "fail-on-severity": "",
+ "registry": {
+ "insecure-skip-tls-verify": false,
+ "insecure-use-http": false,
+ "auth": [],
+ "ca-cert": ""
+ },
+ "log": {
+ "quiet": true,
+ "verbosity": 0,
+ "level": "",
+ "file": ""
+ },
+ "show-suppressed": false,
+ "by-cve": false,
+ "name": "",
+ "default-image-pull-source": ""
+ },
+ "db": {
+ "built": "2023-09-01T01:26:55Z",
+ "schemaVersion": 5,
+ "location": "/Users/willmurphy/Library/Caches/grype/db/5",
+ "checksum": "sha256:5db8bddae95f375db7186527c7554311e9ddc41e815ef2dbc28dc5d206ef2c7b",
+ "error": null
+ },
+ "timestamp": "2023-09-01T08:13:42.20194-04:00"
+ }
+}
diff --git a/grype/presenter/internal/test_helpers.go b/grype/presenter/internal/test_helpers.go
index c1471822b77..4011591e1c7 100644
--- a/grype/presenter/internal/test_helpers.go
+++ b/grype/presenter/internal/test_helpers.go
@@ -6,19 +6,22 @@ import (
"github.com/stretchr/testify/require"
- grypeDb "github.com/anchore/grype/grype/db/v5"
+ "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"
+ "github.com/anchore/grype/grype/vex"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/stereoscope/pkg/image"
- "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"
"github.com/anchore/syft/syft/sbom"
syftSource "github.com/anchore/syft/syft/source"
+ "github.com/anchore/syft/syft/source/directorysource"
+ "github.com/anchore/syft/syft/source/filesource"
+ "github.com/anchore/syft/syft/source/stereoscopesource"
)
const (
@@ -29,52 +32,56 @@ const (
type SyftSource string
-func GenerateAnalysis(t *testing.T, scheme SyftSource) (match.Matches, []pkg.Package, pkg.Context, vulnerability.MetadataProvider, interface{}, interface{}) {
+func GeneratePresenterConfig(t *testing.T, scheme SyftSource) models.PresenterConfig {
+ s, doc := GenerateAnalysis(t, scheme)
+ return models.PresenterConfig{
+ ID: clio.Identification{Name: "grype", Version: "[not provided]"},
+ Document: doc,
+ SBOM: s,
+ Pretty: true,
+ }
+}
+
+func GenerateAnalysis(t *testing.T, scheme SyftSource) (*sbom.SBOM, models.Document) {
t.Helper()
- packages := generatePackages(t)
- matches := generateMatches(t, packages[0], packages[1])
context := generateContext(t, scheme)
- return matches, packages, context, models.NewMetadataMock(), nil, nil
-}
+ s := &sbom.SBOM{
+ Artifacts: sbom.Artifacts{
+ Packages: syftPkg.NewCollection(generatePackages(t)...),
+ },
+ Source: *context.Source,
+ }
-func GenerateAnalysisWithIgnoredMatches(t *testing.T, scheme SyftSource) (match.Matches, []match.IgnoredMatch, []pkg.Package, pkg.Context, vulnerability.MetadataProvider, interface{}, interface{}) {
- t.Helper()
+ grypePackages := pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})
- packages := generatePackages(t)
- matches := generateMatches(t, packages[0], packages[0])
- ignoredMatches := generateIgnoredMatches(t, packages[1])
- context := generateContext(t, scheme)
+ matches := generateMatches(t, grypePackages[0], grypePackages[1])
+
+ doc, err := models.NewDocument(clio.Identification{Name: "grype", Version: "[not provided]"}, grypePackages, context, matches, nil, models.NewMetadataMock(), nil, nil, models.SortByPackage)
+ require.NoError(t, err)
- return matches, ignoredMatches, packages, context, models.NewMetadataMock(), nil, nil
+ return s, doc
}
-func SBOMFromPackages(t *testing.T, packages []pkg.Package) *sbom.SBOM {
+func GenerateAnalysisWithIgnoredMatches(t *testing.T, scheme SyftSource) models.Document {
t.Helper()
- sbom := &sbom.SBOM{
+ s := &sbom.SBOM{
Artifacts: sbom.Artifacts{
- Packages: syftPkg.NewCollection(),
+ Packages: syftPkg.NewCollection(generatePackages(t)...),
},
}
- for _, p := range packages {
- sbom.Artifacts.Packages.Add(toSyftPkg(p))
- }
+ grypePackages := pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})
- return sbom
-}
+ matches := generateMatches(t, grypePackages[0], grypePackages[1])
+ ignoredMatches := generateIgnoredMatches(t, grypePackages[1])
+ context := generateContext(t, scheme)
-func toSyftPkg(p pkg.Package) syftPkg.Package {
- return syftPkg.Package{
- Name: p.Name,
- Version: p.Version,
- Type: p.Type,
- Metadata: p.Metadata,
- Locations: p.Locations,
- CPEs: p.CPEs,
- }
+ doc, err := models.NewDocument(clio.Identification{Name: "grype", Version: "devel"}, grypePackages, context, matches, ignoredMatches, models.NewMetadataMock(), nil, nil, models.SortByPackage)
+ require.NoError(t, err)
+ return doc
}
func Redact(s []byte) []byte {
@@ -83,28 +90,55 @@ func Redact(s []byte) []byte {
refPattern := regexp.MustCompile(`ref="[a-zA-Z0-9\-:]+"`)
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
cycloneDxBomRefPattern := regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
+ tempDirPattern := regexp.MustCompile(`/tmp/[^"]+`)
+ macTempDirPattern := regexp.MustCompile(`/var/folders/[^"]+`)
- for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, refPattern, uuidPattern, cycloneDxBomRefPattern} {
+ for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, refPattern, uuidPattern, cycloneDxBomRefPattern, tempDirPattern, macTempDirPattern} {
s = pattern.ReplaceAll(s, []byte(""))
}
return s
}
-func generateMatches(t *testing.T, p, p2 pkg.Package) match.Matches {
+func generateMatches(t *testing.T, p1, p2 pkg.Package) match.Matches { // nolint:funlen
t.Helper()
matches := []match.Match{
{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-1999-0001",
- Namespace: "source-1",
+ Reference: vulnerability.Reference{
+ ID: "CVE-1999-0001",
+ Namespace: "source-1",
+ },
Fix: vulnerability.Fix{
- Versions: []string{"the-next-version"},
- State: grypeDb.FixedState,
+ 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: p,
+ Package: p1,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
@@ -124,8 +158,38 @@ func generateMatches(t *testing.T, p, p2 pkg.Package) match.Matches {
{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-1999-0002",
- Namespace: "source-2",
+ Reference: vulnerability.Reference{
+ 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{
@@ -148,71 +212,175 @@ func generateMatches(t *testing.T, p, p2 pkg.Package) match.Matches {
return collection
}
+// nolint: funlen
func generateIgnoredMatches(t *testing.T, p pkg.Package) []match.IgnoredMatch {
t.Helper()
- matches := []match.Match{
+ return []match.IgnoredMatch{
{
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-1999-0001",
- Namespace: "source-1",
- },
- Package: p,
- Details: []match.Detail{
- {
- Type: match.ExactDirectMatch,
- Matcher: match.DpkgMatcher,
- SearchedBy: map[string]interface{}{
- "distro": map[string]string{
- "type": "ubuntu",
- "version": "20.04",
+ Match: match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ 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,
+ },
},
},
- Found: map[string]interface{}{
- "constraint": ">= 20",
+ },
+ Package: p,
+ Details: []match.Detail{
+ {
+ Type: match.ExactDirectMatch,
+ Matcher: match.DpkgMatcher,
+ SearchedBy: map[string]interface{}{
+ "distro": map[string]string{
+ "type": "ubuntu",
+ "version": "20.04",
+ },
+ },
+ Found: map[string]interface{}{
+ "constraint": ">= 20",
+ },
},
},
},
+ AppliedIgnoreRules: []match.IgnoreRule{},
},
{
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-1999-0002",
- Namespace: "source-2",
+ Match: match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ 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{
+ {
+ Type: match.ExactDirectMatch,
+ Matcher: match.DpkgMatcher,
+ SearchedBy: map[string]interface{}{
+ "cpe": "somecpe",
+ },
+ Found: map[string]interface{}{
+ "constraint": "somecpe",
+ },
+ },
+ },
},
- Package: p,
- Details: []match.Detail{
- {
- Type: match.ExactDirectMatch,
- Matcher: match.DpkgMatcher,
- SearchedBy: map[string]interface{}{
- "cpe": "somecpe",
+ AppliedIgnoreRules: []match.IgnoreRule{},
+ },
+ {
+ Match: match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-1999-0004",
+ Namespace: "source-2",
},
- Found: map[string]interface{}{
- "constraint": "somecpe",
+ 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{
+ {
+ Type: match.ExactDirectMatch,
+ Matcher: match.DpkgMatcher,
+ SearchedBy: map[string]interface{}{
+ "cpe": "somecpe",
+ },
+ Found: map[string]interface{}{
+ "constraint": "somecpe",
+ },
+ },
+ },
+ },
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Vulnerability: "CVE-1999-0004",
+ Namespace: "vex",
+ Package: match.IgnoreRulePackage{},
+ VexStatus: string(vex.StatusNotAffected),
+ VexJustification: "this isn't the vulnerability match you're looking for... *waves hand*",
+ },
},
},
}
-
- var ignoredMatches []match.IgnoredMatch
- for _, m := range matches {
- ignoredMatches = append(ignoredMatches, match.IgnoredMatch{
- Match: m,
- AppliedIgnoreRules: []match.IgnoreRule{},
- })
- }
-
- return ignoredMatches
}
-func generatePackages(t *testing.T) []pkg.Package {
+func generatePackages(t *testing.T) []syftPkg.Package {
t.Helper()
epoch := 2
- pkgs := []pkg.Package{
+ pkgs := []syftPkg.Package{
{
Name: "package-1",
Version: "1.1.1",
@@ -220,53 +388,50 @@ func generatePackages(t *testing.T) []pkg.Package {
Locations: file.NewLocationSet(file.NewVirtualLocation("/foo/bar/somefile-1.txt", "somefile-1.txt")),
CPEs: []cpe.CPE{
{
- Part: "a",
- Vendor: "anchore",
- Product: "engine",
- Version: "0.9.2",
- Language: "python",
- },
- },
- Upstreams: []pkg.UpstreamPackage{
- {
- Name: "nothing",
- Version: "3.2",
+ Attributes: cpe.Attributes{
+ Part: "a",
+ Vendor: "anchore",
+ Product: "engine",
+ Version: "0.9.2",
+ Language: "en",
+ },
},
},
- MetadataType: pkg.RpmMetadataType,
- Metadata: pkg.RpmMetadata{
- Epoch: &epoch,
+ Metadata: syftPkg.RpmDBEntry{
+ Epoch: &epoch,
+ SourceRpm: "some-source-rpm",
},
},
{
Name: "package-2",
Version: "2.2.2",
Type: syftPkg.DebPkg,
+ PURL: "pkg:deb/package-2@2.2.2",
Locations: file.NewLocationSet(file.NewVirtualLocation("/foo/bar/somefile-2.txt", "somefile-2.txt")),
CPEs: []cpe.CPE{
{
- Part: "a",
- Vendor: "anchore",
- Product: "engine",
- Version: "2.2.2",
- Language: "python",
+ Attributes: cpe.Attributes{
+ Part: "a",
+ Vendor: "anchore",
+ Product: "engine",
+ Version: "2.2.2",
+ Language: "en",
+ },
},
},
- Licenses: []string{"MIT", "Apache-2.0"},
+ Licenses: syftPkg.NewLicenseSet(
+ syftPkg.NewLicense("MIT"),
+ syftPkg.NewLicense("Apache-2.0"),
+ ),
},
}
- updatedPkgs := make([]pkg.Package, 0, len(pkgs))
-
- for _, p := range pkgs {
- id, err := artifact.IDByHash(p)
- require.NoError(t, err)
-
- p.ID = pkg.ID(id)
- updatedPkgs = append(updatedPkgs, p)
+ for i := range pkgs {
+ p := pkgs[i]
+ p.SetID()
}
- return updatedPkgs
+ return pkgs
}
//nolint:funlen
@@ -279,9 +444,7 @@ func generateContext(t *testing.T, scheme SyftSource) pkg.Context {
switch scheme {
case FileSource:
var err error
- src, err = syftSource.NewFromFile(syftSource.FileConfig{
- Path: "user-input",
- })
+ src, err = filesource.NewFromPath("user-input")
if err != nil {
t.Fatalf("failed to generate mock file source from mock image: %+v", err)
}
@@ -319,25 +482,21 @@ func generateContext(t *testing.T, scheme SyftSource) pkg.Context {
},
}
- var err error
- src, err = syftSource.NewFromStereoscopeImageObject(&img, "user-input", nil)
- if err != nil {
- t.Fatalf("failed to generate mock image source from mock image: %+v", err)
- }
+ src = stereoscopesource.New(&img, stereoscopesource.ImageConfig{
+ Reference: "user-input",
+ })
desc = src.Describe()
case DirectorySource:
// note: the dir must exist for the source to be created
d := t.TempDir()
var err error
- src, err = syftSource.NewFromDirectory(syftSource.DirectoryConfig{
- Path: d,
- })
+ src, err = directorysource.NewFromPath(d)
if err != nil {
t.Fatalf("failed to generate mock directory source from mock dir: %+v", err)
}
desc = src.Describe()
- if m, ok := desc.Metadata.(syftSource.DirectorySourceMetadata); ok {
+ if m, ok := desc.Metadata.(syftSource.DirectoryMetadata); ok {
m.Path = "/some/path"
desc.Metadata = m
}
@@ -347,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 8b26a35f895..27ca2a912d5 100644
--- a/grype/presenter/json/presenter.go
+++ b/grype/presenter/json/presenter.go
@@ -4,47 +4,27 @@ import (
"encoding/json"
"io"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/presenter/models"
- "github.com/anchore/grype/grype/vulnerability"
)
-// Presenter is a generic struct for holding fields needed for reporting
type Presenter struct {
- matches match.Matches
- ignoredMatches []match.IgnoredMatch
- packages []pkg.Package
- context pkg.Context
- metadataProvider vulnerability.MetadataProvider
- appConfig interface{}
- dbStatus interface{}
+ document models.Document
+ pretty bool
}
-// NewPresenter creates a new JSON presenter
func NewPresenter(pb models.PresenterConfig) *Presenter {
return &Presenter{
- matches: pb.Matches,
- ignoredMatches: pb.IgnoredMatches,
- packages: pb.Packages,
- metadataProvider: pb.MetadataProvider,
- context: pb.Context,
- appConfig: pb.AppConfig,
- dbStatus: pb.DBStatus,
+ document: pb.Document,
+ pretty: pb.Pretty,
}
}
-// Present creates a JSON-based reporting
-func (pres *Presenter) Present(output io.Writer) error {
- doc, err := models.NewDocument(pres.packages, pres.context, pres.matches, pres.ignoredMatches, pres.metadataProvider,
- pres.appConfig, pres.dbStatus)
- if err != nil {
- return err
- }
-
+func (p *Presenter) Present(output io.Writer) error {
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
- enc.SetIndent("", " ")
- return enc.Encode(&doc)
+ if p.pretty {
+ enc.SetIndent("", " ")
+ }
+ return enc.Encode(&p.document)
}
diff --git a/grype/presenter/json/presenter_test.go b/grype/presenter/json/presenter_test.go
index 7806ba60ef1..dc2929ae518 100644
--- a/grype/presenter/json/presenter_test.go
+++ b/grype/presenter/json/presenter_test.go
@@ -6,14 +6,17 @@ import (
"regexp"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "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"
)
@@ -22,14 +25,8 @@ var timestampRegexp = regexp.MustCompile(`"timestamp":\s*"[^"]+"`)
func TestJsonImgsPresenter(t *testing.T) {
var buffer bytes.Buffer
- matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.ImageSource)
- pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- Context: context,
- MetadataProvider: metadataProvider,
- }
+ pb := internal.GeneratePresenterConfig(t, internal.ImageSource)
pres := NewPresenter(pb)
@@ -46,7 +43,9 @@ func TestJsonImgsPresenter(t *testing.T) {
var expected = testutils.GetGoldenFileContents(t)
- assert.JSONEq(t, string(expected), string(actual))
+ if d := cmp.Diff(string(expected), string(actual)); d != "" {
+ t.Fatalf("diff: %s", d)
+ }
// TODO: add me back in when there is a JSON schema
// validateAgainstDbSchema(t, string(actual))
@@ -55,14 +54,7 @@ func TestJsonImgsPresenter(t *testing.T) {
func TestJsonDirsPresenter(t *testing.T) {
var buffer bytes.Buffer
- matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.DirectorySource)
-
- pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- Context: context,
- MetadataProvider: metadataProvider,
- }
+ pb := internal.GeneratePresenterConfig(t, internal.DirectorySource)
pres := NewPresenter(pb)
@@ -79,7 +71,9 @@ func TestJsonDirsPresenter(t *testing.T) {
var expected = testutils.GetGoldenFileContents(t)
- assert.JSONEq(t, string(expected), string(actual))
+ if d := cmp.Diff(string(expected), string(actual)); d != "" {
+ t.Fatalf("diff: %s", d)
+ }
// TODO: add me back in when there is a JSON schema
// validateAgainstDbSchema(t, string(actual))
@@ -89,22 +83,24 @@ func TestEmptyJsonPresenter(t *testing.T) {
// Expected to have an empty JSON array back
var buffer bytes.Buffer
- matches := match.NewMatches()
-
ctx := pkg.Context{
Source: &source.Description{},
- Distro: &linux.Release{
- ID: "centos",
+ Distro: &distro.Distro{
+ Type: "centos",
IDLike: []string{"rhel"},
Version: "8.0",
},
}
+ doc, err := models.NewDocument(clio.Identification{Name: "grype", Version: "[not provided]"}, nil, ctx, match.NewMatches(), nil, models.NewMetadataMock(), nil, nil, models.SortByPackage)
+ require.NoError(t, err)
+
pb := models.PresenterConfig{
- Matches: matches,
- Packages: nil,
- Context: ctx,
- MetadataProvider: nil,
+ ID: clio.Identification{
+ Name: "grype",
+ Version: "[not provided]",
+ },
+ Document: doc,
}
pres := NewPresenter(pb)
diff --git a/grype/presenter/json/test-fixtures/snapshot/TestEmptyJsonPresenter.golden b/grype/presenter/json/test-fixtures/snapshot/TestEmptyJsonPresenter.golden
index b10e45c5ec7..8a816da1572 100644
--- a/grype/presenter/json/test-fixtures/snapshot/TestEmptyJsonPresenter.golden
+++ b/grype/presenter/json/test-fixtures/snapshot/TestEmptyJsonPresenter.golden
@@ -1,19 +1 @@
-{
- "matches": [],
- "source": {
- "type": "unknown",
- "target": "unknown"
- },
- "distro": {
- "name": "centos",
- "version": "8.0",
- "idLike": [
- "rhel"
- ]
- },
- "descriptor": {
- "name": "grype",
- "version": "[not provided]",
- "timestamp":""
- }
-}
+{"matches":[],"source":{"type":"unknown","target":"unknown"},"distro":{"name":"centos","version":"8.0","idLike":["rhel"]},"descriptor":{"name":"grype","version":"[not provided]","timestamp":""}}
diff --git a/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden b/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden
index 840e58c3e60..8d2b44b5d39 100644
--- a/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden
+++ b/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden
@@ -6,24 +6,36 @@
"dataSource": "",
"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": [
- "the-next-version"
+ "1.2.1",
+ "2.1.3",
+ "3.4.0"
],
"state": "fixed"
},
- "advisories": []
+ "advisories": [],
+ "risk": 1.68
},
"relatedVulnerabilities": [],
"matchDetails": [
@@ -38,35 +50,34 @@
},
"found": {
"constraint": ">= 20"
+ },
+ "fix": {
+ "suggestedVersion": "1.2.1"
}
}
],
"artifact": {
- "id": "96699b00fe3004b4",
+ "id": "bbb0ba712c2b94ea",
"name": "package-1",
"version": "1.1.1",
"type": "rpm",
"locations": [
{
- "path": "/foo/bar/somefile-1.txt"
+ "path": "/foo/bar/somefile-1.txt",
+ "accessPath": "somefile-1.txt"
}
],
"language": "",
"licenses": [],
"cpes": [
- "cpe:2.3:a:anchore:engine:0.9.2:*:*:python:*:*:*:*"
+ "cpe:2.3:a:anchore:engine:0.9.2:*:*:en:*:*:*:*"
],
"purl": "",
- "upstreams": [
- {
- "name": "nothing",
- "version": "3.2"
- }
- ],
+ "upstreams": [],
"metadataType": "RpmMetadata",
"metadata": {
"epoch": 2,
- "modularityLabel": ""
+ "modularityLabel": null
}
}
},
@@ -76,27 +87,38 @@
"dataSource": "",
"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": [
@@ -112,24 +134,25 @@
}
],
"artifact": {
- "id": "b4013a965511376c",
+ "id": "74378afe15713625",
"name": "package-2",
"version": "2.2.2",
"type": "deb",
"locations": [
{
- "path": "/foo/bar/somefile-2.txt"
+ "path": "/foo/bar/somefile-2.txt",
+ "accessPath": "somefile-2.txt"
}
],
"language": "",
"licenses": [
- "MIT",
- "Apache-2.0"
+ "Apache-2.0",
+ "MIT"
],
"cpes": [
- "cpe:2.3:a:anchore:engine:2.2.2:*:*:python:*:*:*:*"
+ "cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*"
],
- "purl": "",
+ "purl": "pkg:deb/package-2@2.2.2",
"upstreams": []
}
}
diff --git a/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden b/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden
index fb81e431ce9..d7669215c0c 100644
--- a/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden
+++ b/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden
@@ -6,24 +6,36 @@
"dataSource": "",
"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": [
- "the-next-version"
+ "1.2.1",
+ "2.1.3",
+ "3.4.0"
],
"state": "fixed"
},
- "advisories": []
+ "advisories": [],
+ "risk": 1.68
},
"relatedVulnerabilities": [],
"matchDetails": [
@@ -38,35 +50,34 @@
},
"found": {
"constraint": ">= 20"
+ },
+ "fix": {
+ "suggestedVersion": "1.2.1"
}
}
],
"artifact": {
- "id": "96699b00fe3004b4",
+ "id": "bbb0ba712c2b94ea",
"name": "package-1",
"version": "1.1.1",
"type": "rpm",
"locations": [
{
- "path": "/foo/bar/somefile-1.txt"
+ "path": "/foo/bar/somefile-1.txt",
+ "accessPath": "somefile-1.txt"
}
],
"language": "",
"licenses": [],
"cpes": [
- "cpe:2.3:a:anchore:engine:0.9.2:*:*:python:*:*:*:*"
+ "cpe:2.3:a:anchore:engine:0.9.2:*:*:en:*:*:*:*"
],
"purl": "",
- "upstreams": [
- {
- "name": "nothing",
- "version": "3.2"
- }
- ],
+ "upstreams": [],
"metadataType": "RpmMetadata",
"metadata": {
"epoch": 2,
- "modularityLabel": ""
+ "modularityLabel": null
}
}
},
@@ -76,27 +87,38 @@
"dataSource": "",
"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": [
@@ -112,24 +134,25 @@
}
],
"artifact": {
- "id": "b4013a965511376c",
+ "id": "74378afe15713625",
"name": "package-2",
"version": "2.2.2",
"type": "deb",
"locations": [
{
- "path": "/foo/bar/somefile-2.txt"
+ "path": "/foo/bar/somefile-2.txt",
+ "accessPath": "somefile-2.txt"
}
],
"language": "",
"licenses": [
- "MIT",
- "Apache-2.0"
+ "Apache-2.0",
+ "MIT"
],
"cpes": [
- "cpe:2.3:a:anchore:engine:2.2.2:*:*:python:*:*:*:*"
+ "cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*"
],
- "purl": "",
+ "purl": "pkg:deb/package-2@2.2.2",
"upstreams": []
}
}
diff --git a/grype/presenter/models/cvss.go b/grype/presenter/models/cvss.go
index 582901a1a1a..e1338a91d0f 100644
--- a/grype/presenter/models/cvss.go
+++ b/grype/presenter/models/cvss.go
@@ -17,7 +17,7 @@ type CvssMetrics struct {
ImpactScore *float64 `json:"impactScore,omitempty"`
}
-func NewCVSS(metadata *vulnerability.Metadata) []Cvss {
+func toCVSS(metadata *vulnerability.Metadata) []Cvss {
cvss := make([]Cvss, 0)
for _, score := range metadata.Cvss {
vendorMetadata := score.VendorMetadata
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 13100da14af..068486d1eb1 100644
--- a/grype/presenter/models/document.go
+++ b/grype/presenter/models/document.go
@@ -4,11 +4,10 @@ import (
"fmt"
"time"
+ "github.com/anchore/clio"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/grype/internal"
- "github.com/anchore/grype/internal/version"
)
// Document represents the JSON document to be presented
@@ -21,7 +20,7 @@ type Document struct {
}
// NewDocument creates and populates a new Document struct, representing the populated JSON document.
-func NewDocument(packages []pkg.Package, context pkg.Context, matches match.Matches, ignoredMatches []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider, appConfig interface{}, dbStatus interface{}) (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
@@ -43,6 +42,8 @@ func NewDocument(packages []pkg.Package, context pkg.Context, matches match.Matc
findings = append(findings, *matchModel)
}
+ SortMatches(findings, strategy)
+
var src *source
if context.Source != nil {
theSrc, err := newSource(*context.Source)
@@ -77,11 +78,11 @@ func NewDocument(packages []pkg.Package, context pkg.Context, matches match.Matc
Source: src,
Distro: newDistribution(context.Distro),
Descriptor: descriptor{
- Name: internal.ApplicationName,
- Version: version.FromBuild().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 588a074cf89..0cb95916935 100644
--- a/grype/presenter/models/document_test.go
+++ b/grype/presenter/models/document_test.go
@@ -6,16 +6,16 @@ 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"
)
func TestPackagesAreSorted(t *testing.T) {
-
var pkg1 = pkg.Package{
ID: "package-1-id",
Name: "package-1",
@@ -32,7 +32,7 @@ func TestPackagesAreSorted(t *testing.T) {
var match1 = match.Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-1999-0003",
+ Reference: vulnerability.Reference{ID: "CVE-1999-0003"},
},
Package: pkg1,
Details: match.Details{
@@ -44,9 +44,9 @@ func TestPackagesAreSorted(t *testing.T) {
var match2 = match.Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-1999-0002",
+ Reference: vulnerability.Reference{ID: "CVE-1999-0002"},
},
- Package: pkg1,
+ Package: pkg2,
Details: match.Details{
{
Type: match.ExactIndirectMatch,
@@ -56,7 +56,7 @@ func TestPackagesAreSorted(t *testing.T) {
var match3 = match.Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-1999-0001",
+ Reference: vulnerability.Reference{ID: "CVE-1999-0001"},
},
Package: pkg1,
Details: match.Details{
@@ -73,25 +73,83 @@ func TestPackagesAreSorted(t *testing.T) {
ctx := pkg.Context{
Source: &syftSource.Description{
- Metadata: syftSource.DirectorySourceMetadata{},
+ Metadata: syftSource.DirectoryMetadata{},
},
- Distro: &linux.Release{
- ID: "centos",
+ Distro: &distro.Distro{
+ Type: "centos",
IDLike: []string{"rhel"},
Version: "8.0",
},
}
- doc, err := NewDocument(packages, ctx, matches, nil, NewMetadataMock(), nil, nil)
+ doc, err := NewDocument(clio.Identification{}, packages, ctx, matches, nil, NewMetadataMock(), nil, nil, SortByPackage)
if err != nil {
t.Fatalf("unable to get document: %+v", err)
}
+ var actualPackages []string
+ for _, m := range doc.Matches {
+ actualPackages = append(actualPackages, m.Artifact.Name)
+ }
+
+ // sort packages first
+ assert.Equal(t, []string{"package-1", "package-1", "package-2"}, actualPackages)
+
var actualVulnerabilities []string
for _, m := range doc.Matches {
actualVulnerabilities = append(actualVulnerabilities, m.Vulnerability.ID)
}
- assert.Equal(t, []string{"CVE-1999-0001", "CVE-1999-0002", "CVE-1999-0003"}, actualVulnerabilities)
+ // sort vulnerabilities second
+ assert.Equal(t, []string{"CVE-1999-0001", "CVE-1999-0003", "CVE-1999-0002"}, actualVulnerabilities)
+}
+
+func TestFixSuggestedVersion(t *testing.T) {
+
+ var pkg1 = pkg.Package{
+ ID: "package-1-id",
+ Name: "package-1",
+ Version: "1.1.1",
+ Type: syftPkg.PythonPkg,
+ }
+
+ var match1 = match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Fix: vulnerability.Fix{
+ Versions: []string{"1.0.0", "1.2.0", "1.1.2"},
+ },
+ Reference: vulnerability.Reference{ID: "CVE-1999-0003"},
+ },
+ Package: pkg1,
+ Details: match.Details{
+ {
+ Type: match.ExactDirectMatch,
+ },
+ },
+ }
+
+ matches := match.NewMatches()
+ matches.Add(match1)
+
+ packages := []pkg.Package{pkg1}
+
+ ctx := pkg.Context{
+ Source: &syftSource.Description{
+ Metadata: syftSource.DirectoryMetadata{},
+ },
+ Distro: &distro.Distro{
+ Type: "centos",
+ IDLike: []string{"rhel"},
+ Version: "8.0",
+ },
+ }
+ doc, err := NewDocument(clio.Identification{}, packages, ctx, matches, nil, NewMetadataMock(), nil, nil, SortByPackage)
+ if err != nil {
+ t.Fatalf("unable to get document: %+v", err)
+ }
+
+ actualSuggestedFixedVersion := doc.Matches[0].MatchDetails[0].Fix.SuggestedVersion
+
+ assert.Equal(t, "1.1.2", actualSuggestedFixedVersion)
}
func TestTimestampValidFormat(t *testing.T) {
@@ -103,7 +161,7 @@ func TestTimestampValidFormat(t *testing.T) {
Distro: nil,
}
- doc, err := NewDocument(nil, ctx, matches, nil, nil, nil, nil)
+ doc, err := NewDocument(clio.Identification{}, nil, ctx, matches, nil, nil, nil, nil, SortByPackage)
if err != nil {
t.Fatalf("unable to get document: %+v", err)
}
diff --git a/grype/presenter/models/ignore.go b/grype/presenter/models/ignore.go
index 4f4176945e4..d3b8940099e 100644
--- a/grype/presenter/models/ignore.go
+++ b/grype/presenter/models/ignore.go
@@ -8,16 +8,23 @@ type IgnoredMatch struct {
}
type IgnoreRule struct {
- Vulnerability string `json:"vulnerability,omitempty"`
- FixState string `json:"fix-state,omitempty"`
- Package *IgnoreRulePackage `json:"package,omitempty"`
+ Vulnerability string `json:"vulnerability,omitempty"`
+ Reason string `json:"reason,omitempty"`
+ Namespace string `json:"namespace"`
+ FixState string `json:"fix-state,omitempty"`
+ Package *IgnoreRulePackage `json:"package,omitempty"`
+ VexStatus string `json:"vex-status,omitempty"`
+ VexJustification string `json:"vex-justification,omitempty"`
+ MatchType string `json:"match-type,omitempty"`
}
type IgnoreRulePackage struct {
- Name string `json:"name,omitempty"`
- Version string `json:"version,omitempty"`
- Type string `json:"type,omitempty"`
- Location string `json:"location,omitempty"`
+ Name string `json:"name,omitempty"`
+ Version string `json:"version,omitempty"`
+ Language string `json:"language"`
+ Type string `json:"type,omitempty"`
+ Location string `json:"location,omitempty"`
+ UpstreamName string `json:"upstream-name,omitempty"`
}
func newIgnoreRule(r match.IgnoreRule) IgnoreRule {
@@ -26,17 +33,24 @@ func newIgnoreRule(r match.IgnoreRule) IgnoreRule {
// We'll only set the package part of the rule not to `nil` if there are any values to fill out.
if p := r.Package; p.Name != "" || p.Version != "" || p.Type != "" || p.Location != "" {
ignoreRulePackage = &IgnoreRulePackage{
- Name: r.Package.Name,
- Version: r.Package.Version,
- Type: r.Package.Type,
- Location: r.Package.Location,
+ Name: r.Package.Name,
+ Version: r.Package.Version,
+ Language: r.Package.Language,
+ Type: r.Package.Type,
+ Location: r.Package.Location,
+ UpstreamName: r.Package.UpstreamName,
}
}
return IgnoreRule{
- Vulnerability: r.Vulnerability,
- FixState: r.FixState,
- Package: ignoreRulePackage,
+ Vulnerability: r.Vulnerability,
+ Reason: r.Reason,
+ Namespace: r.Namespace,
+ FixState: r.FixState,
+ Package: ignoreRulePackage,
+ VexStatus: r.VexStatus,
+ VexJustification: r.VexJustification,
+ MatchType: string(r.MatchType),
}
}
diff --git a/grype/presenter/models/ignore_test.go b/grype/presenter/models/ignore_test.go
index e7fa8ef80d8..1243a4dad51 100644
--- a/grype/presenter/models/ignore_test.go
+++ b/grype/presenter/models/ignore_test.go
@@ -5,8 +5,8 @@ import (
"github.com/google/go-cmp/cmp"
- grypeDb "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/vulnerability"
)
func TestNewIgnoreRule(t *testing.T) {
@@ -36,10 +36,10 @@ func TestNewIgnoreRule(t *testing.T) {
{
name: "only fix state field",
input: match.IgnoreRule{
- FixState: string(grypeDb.NotFixedState),
+ FixState: string(vulnerability.FixStateNotFixed),
},
expected: IgnoreRule{
- FixState: string(grypeDb.NotFixedState),
+ FixState: string(vulnerability.FixStateNotFixed),
},
},
@@ -79,7 +79,7 @@ func TestNewIgnoreRule(t *testing.T) {
name: "all fields",
input: match.IgnoreRule{
Vulnerability: "CVE-2020-1234",
- FixState: string(grypeDb.NotFixedState),
+ FixState: string(vulnerability.FixStateNotFixed),
Package: match.IgnoreRulePackage{
Name: "libc",
Version: "3.0.0",
diff --git a/grype/presenter/models/match.go b/grype/presenter/models/match.go
index 10ec2934248..a90ba7c0d11 100644
--- a/grype/presenter/models/match.go
+++ b/grype/presenter/models/match.go
@@ -6,7 +6,9 @@ import (
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
)
// Match is a single item for the JSON array reported
@@ -21,14 +23,20 @@ type Match struct {
type MatchDetails struct {
Type string `json:"type"`
Matcher string `json:"matcher"`
- SearchedBy interface{} `json:"searchedBy"`
- Found interface{} `json:"found"`
+ SearchedBy interface{} `json:"searchedBy"` // The specific attributes that were used to search (other than package name and version) --this indicates "how" the match was made.
+ Found interface{} `json:"found"` // The specific attributes on the vulnerability object that were matched with --this indicates "what" was matched on / within.
+ Fix *FixDetails `json:"fix,omitempty"`
+}
+
+// FixDetails contains any data that is relevant to fixing the vulnerability specific to the package searched with
+type FixDetails struct {
+ SuggestedVersion string `json:"suggestedVersion"`
}
func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.MetadataProvider) (*Match, error) {
relatedVulnerabilities := make([]VulnerabilityMetadata, 0)
for _, r := range m.Vulnerability.RelatedVulnerabilities {
- relatedMetadata, err := metadataProvider.GetMetadata(r.ID, r.Namespace)
+ relatedMetadata, err := metadataProvider.VulnerabilityMetadata(r)
if err != nil {
return nil, fmt.Errorf("unable to fetch related vuln=%q metadata: %+v", r, err)
}
@@ -37,11 +45,19 @@ func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.Metad
}
}
- metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
- if err != nil {
- return nil, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
+ // vulnerability.Vulnerability should always have vulnerability.Metadata populated, however, in the case of test mocks
+ // and other edge cases, it may not be populated. In these cases, we should fetch the metadata from the provider.
+ metadata := m.Vulnerability.Metadata
+ if metadata == nil {
+ var err error
+ metadata, err = metadataProvider.VulnerabilityMetadata(m.Vulnerability.Reference)
+ if err != nil {
+ return nil, fmt.Errorf("unable to fetch related vuln=%q metadata: %+v", m.Vulnerability.Reference, err)
+ }
}
+ format := version.FormatFromPkg(p)
+
details := make([]MatchDetails, len(m.Details))
for idx, d := range m.Details {
details[idx] = MatchDetails{
@@ -49,41 +65,79 @@ func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.Metad
Matcher: string(d.Matcher),
SearchedBy: d.SearchedBy,
Found: d.Found,
+ Fix: getFix(m, p, format),
}
}
return &Match{
- Vulnerability: NewVulnerability(m.Vulnerability, metadata),
+ Vulnerability: NewVulnerability(m.Vulnerability, metadata, format),
Artifact: newPackage(p),
RelatedVulnerabilities: relatedVulnerabilities,
MatchDetails: details,
}, nil
}
-var _ sort.Interface = (*ByName)(nil)
+func getFix(m match.Match, p pkg.Package, format version.Format) *FixDetails {
+ suggested := calculateSuggestedFixedVersion(p, m.Vulnerability.Fix.Versions, format)
+ if suggested == "" {
+ return nil
+ }
+ return &FixDetails{
+ SuggestedVersion: suggested,
+ }
+}
-type ByName []Match
+func calculateSuggestedFixedVersion(p pkg.Package, fixedVersions []string, format version.Format) string {
+ if len(fixedVersions) == 0 {
+ return ""
+ }
-// Len is the number of elements in the collection.
-func (m ByName) Len() int {
- return len(m)
-}
+ if len(fixedVersions) == 1 {
+ return fixedVersions[0]
+ }
-// Less reports whether the element with index i should sort before the element with index j.
-func (m ByName) Less(i, j int) bool {
- if m[i].Artifact.Name == m[j].Artifact.Name {
- if m[i].Vulnerability.ID == m[j].Vulnerability.ID {
- if m[i].Artifact.Version == m[j].Artifact.Version {
- return m[i].Artifact.Type < m[j].Artifact.Type
- }
- return m[i].Artifact.Version < m[j].Artifact.Version
+ parseConstraint := func(constStr string) (version.Constraint, error) {
+ constraint, err := version.GetConstraint(constStr, format)
+ if err != nil {
+ log.WithFields("package", p.Name).Trace("skipping sorting fixed versions")
}
- return m[i].Vulnerability.ID < m[j].Vulnerability.ID
+ return constraint, err
}
- return m[i].Artifact.Name < m[j].Artifact.Name
-}
-// Swap swaps the elements with indexes i and j.
-func (m ByName) Swap(i, j int) {
- m[i], m[j] = m[j], m[i]
+ checkSatisfaction := func(constraint version.Constraint, v *version.Version) bool {
+ satisfied, err := constraint.Satisfied(v)
+ if err != nil {
+ log.WithFields("package", p.Name).Trace("error while checking version satisfaction for sorting")
+ }
+ return satisfied && err == nil
+ }
+
+ sort.SliceStable(fixedVersions, func(i, j int) bool {
+ v1, err1 := version.NewVersion(fixedVersions[i], format)
+ v2, err2 := version.NewVersion(fixedVersions[j], format)
+ if err1 != nil || err2 != nil {
+ log.WithFields("package", p.Name).Trace("error while parsing version for sorting")
+ return false
+ }
+
+ packageConstraint, err := parseConstraint(fmt.Sprintf("<=%s", p.Version))
+ if err != nil {
+ return false
+ }
+
+ v1Satisfied := checkSatisfaction(packageConstraint, v1)
+ v2Satisfied := checkSatisfaction(packageConstraint, v2)
+
+ if v1Satisfied != v2Satisfied {
+ return !v1Satisfied
+ }
+
+ internalConstraint, err := parseConstraint(fmt.Sprintf("<=%s", v1.Raw))
+ if err != nil {
+ return false
+ }
+ return !checkSatisfaction(internalConstraint, v2)
+ })
+
+ return fixedVersions[0]
}
diff --git a/grype/presenter/models/metadata_mock.go b/grype/presenter/models/metadata_mock.go
index 326fec60f9e..4a2f16dc5e6 100644
--- a/grype/presenter/models/metadata_mock.go
+++ b/grype/presenter/models/metadata_mock.go
@@ -1,10 +1,12 @@
package models
-import "github.com/anchore/grype/grype/vulnerability"
+import (
+ "github.com/anchore/grype/grype/vulnerability"
+)
var _ vulnerability.MetadataProvider = (*MetadataMock)(nil)
-// MetadataMock provides the behavior required for a vulnerability.MetadataProvider for the purpose of testing.
+// MetadataMock provides the behavior required for a vulnerability.Provider for the purpose of testing.
type MetadataMock struct {
store map[string]map[string]vulnerability.Metadata
}
@@ -39,11 +41,11 @@ func NewMetadataMock() *MetadataMock {
Severity: "Critical",
Cvss: []vulnerability.Cvss{
{
- Metrics: vulnerability.NewCvssMetrics(
- 1,
- 2,
- 3,
- ),
+ Metrics: vulnerability.CvssMetrics{
+ BaseScore: 1,
+ ExploitabilityScore: ptr(2.0),
+ ImpactScore: ptr(3.0),
+ },
Vector: "vector",
Version: "2.0",
VendorMetadata: MockVendorMetadata{
@@ -60,12 +62,39 @@ func NewMetadataMock() *MetadataMock {
Severity: "High",
},
},
+ "CVE-1999-0004": {
+ "source-2": {
+ Description: "1999-04 description",
+ Severity: "Critical",
+ Cvss: []vulnerability.Cvss{
+ {
+ Metrics: vulnerability.CvssMetrics{
+ BaseScore: 1,
+ ExploitabilityScore: ptr(2.0),
+ ImpactScore: ptr(3.0),
+ },
+ Vector: "vector",
+ Version: "2.0",
+ VendorMetadata: MockVendorMetadata{
+ BaseSeverity: "Low",
+ Status: "verified",
+ },
+ },
+ },
+ },
+ },
},
}
}
-// GetMetadata returns vulnerability metadata for a given id and recordSource.
-func (m *MetadataMock) GetMetadata(id, namespace string) (*vulnerability.Metadata, error) {
- value := m.store[id][namespace]
+func ptr[T any](t T) *T {
+ return &t
+}
+
+// VulnerabilityMetadata returns vulnerability metadata for a given id and recordSource.
+func (m *MetadataMock) VulnerabilityMetadata(vuln vulnerability.Reference) (*vulnerability.Metadata, error) {
+ value := m.store[vuln.ID][vuln.Namespace]
+ value.ID = vuln.ID
+ value.Namespace = vuln.Namespace
return &value, nil
}
diff --git a/grype/presenter/models/package.go b/grype/presenter/models/package.go
index 43e0c1aa6e9..47cd920e27a 100644
--- a/grype/presenter/models/package.go
+++ b/grype/presenter/models/package.go
@@ -1,6 +1,7 @@
package models
import (
+ "github.com/anchore/grype/grype/internal/packagemetadata"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/syft/syft/file"
syftPkg "github.com/anchore/syft/syft/pkg"
@@ -8,18 +9,18 @@ import (
// Package is meant to be only the fields that are needed when displaying a single pkg.Package object for the JSON presenter.
type Package struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Version string `json:"version"`
- Type syftPkg.Type `json:"type"`
- Locations []file.Coordinates `json:"locations"`
- Language syftPkg.Language `json:"language"`
- Licenses []string `json:"licenses"`
- CPEs []string `json:"cpes"`
- PURL string `json:"purl"`
- Upstreams []UpstreamPackage `json:"upstreams"`
- MetadataType pkg.MetadataType `json:"metadataType,omitempty"`
- Metadata interface{} `json:"metadata,omitempty"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Type syftPkg.Type `json:"type"`
+ Locations file.Locations `json:"locations"`
+ Language syftPkg.Language `json:"language"`
+ Licenses []string `json:"licenses"`
+ CPEs []string `json:"cpes"`
+ PURL string `json:"purl"`
+ Upstreams []UpstreamPackage `json:"upstreams"`
+ MetadataType string `json:"metadataType,omitempty"`
+ Metadata interface{} `json:"metadata,omitempty"`
}
type UpstreamPackage struct {
@@ -30,7 +31,7 @@ type UpstreamPackage struct {
func newPackage(p pkg.Package) Package {
var cpes = make([]string, 0)
for _, c := range p.CPEs {
- cpes = append(cpes, c.BindToFmtString())
+ cpes = append(cpes, c.Attributes.BindToFmtString())
}
licenses := p.Licenses
@@ -38,12 +39,6 @@ func newPackage(p pkg.Package) Package {
licenses = make([]string, 0)
}
- var coordinates = make([]file.Coordinates, 0)
- locations := p.Locations.ToSlice()
- for _, l := range locations {
- coordinates = append(coordinates, l.Coordinates)
- }
-
var upstreams = make([]UpstreamPackage, 0)
for _, u := range p.Upstreams {
upstreams = append(upstreams, UpstreamPackage{
@@ -56,14 +51,14 @@ func newPackage(p pkg.Package) Package {
ID: string(p.ID),
Name: p.Name,
Version: p.Version,
- Locations: coordinates,
+ Locations: p.Locations.ToSlice(),
Licenses: licenses,
Language: p.Language,
Type: p.Type,
CPEs: cpes,
PURL: p.PURL,
Upstreams: upstreams,
- MetadataType: p.MetadataType,
+ MetadataType: packagemetadata.JSONName(p.Metadata),
Metadata: p.Metadata,
}
}
diff --git a/grype/presenter/models/presenter_bundle.go b/grype/presenter/models/presenter_bundle.go
index dc2c384cfda..106e963592c 100644
--- a/grype/presenter/models/presenter_bundle.go
+++ b/grype/presenter/models/presenter_bundle.go
@@ -1,19 +1,13 @@
package models
import (
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/clio"
"github.com/anchore/syft/syft/sbom"
)
type PresenterConfig struct {
- Matches match.Matches
- IgnoredMatches []match.IgnoredMatch
- Packages []pkg.Package
- Context pkg.Context
- MetadataProvider vulnerability.MetadataProvider
- SBOM *sbom.SBOM
- AppConfig interface{}
- DBStatus interface{}
+ ID clio.Identification
+ Document Document
+ SBOM *sbom.SBOM
+ Pretty bool
}
diff --git a/grype/presenter/models/sort.go b/grype/presenter/models/sort.go
new file mode 100644
index 00000000000..f1c5895e44f
--- /dev/null
+++ b/grype/presenter/models/sort.go
@@ -0,0 +1,292 @@
+package models
+
+import (
+ "sort"
+ "strings"
+
+ "github.com/anchore/grype/internal/log"
+)
+
+type SortStrategy string
+
+const (
+ SortByPackage SortStrategy = "package"
+ SortBySeverity SortStrategy = "severity"
+ SortByThreat SortStrategy = "epss"
+ SortByRisk SortStrategy = "risk"
+ SortByKEV SortStrategy = "kev"
+ SortByVulnerability SortStrategy = "vulnerability"
+
+ DefaultSortStrategy = SortByRisk
+)
+
+func SortStrategies() []SortStrategy {
+ return []SortStrategy{SortByPackage, SortBySeverity, SortByThreat, SortByRisk, SortByKEV, SortByVulnerability}
+}
+
+func (s SortStrategy) String() string {
+ return string(s)
+}
+
+// compareFunc defines a comparison function between two Match values
+// Returns:
+//
+// -1: if a should come before b
+// 0: if a and b are equal for this comparison
+// 1: if a should come after b
+type compareFunc func(a, b Match) int
+
+// sortStrategyImpl defines a strategy for sorting with a slice of comparison functions
+type sortStrategyImpl []compareFunc
+
+// matchSortStrategy provides predefined sort strategies for Match
+var matchSortStrategy = map[SortStrategy]sortStrategyImpl{
+ SortByPackage: {
+ 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))
+}
+
+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]
+ }
+ return strategy
+}
+
+func sortWithStrategy(matches []Match, strategy sortStrategyImpl) {
+ sort.Slice(matches, func(i, j int) bool {
+ for _, compare := range strategy {
+ result := compare(matches[i], matches[j])
+ if result != 0 {
+ // we are implementing a "less" function, so we want to return true if the result is negative
+ return result < 0
+ }
+ }
+ return false // all comparisons are equal
+ })
+}
+
+func compareByVulnerabilityID(a, b Match) int {
+ aID := a.Vulnerability.ID
+ bID := b.Vulnerability.ID
+
+ switch {
+ case aID < bID:
+ return -1
+ case aID > bID:
+ return 1
+ default:
+ return 0
+ }
+}
+
+func compareBySeverity(a, b Match) int {
+ 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
+ return -1
+ case aScore < bScore:
+ return 1
+ default:
+ return 0
+ }
+}
+
+func compareByPackageName(a, b Match) int {
+ aName := a.Artifact.Name
+ bName := b.Artifact.Name
+
+ switch {
+ case aName < bName:
+ return -1
+ case aName > bName:
+ return 1
+ default:
+ return 0
+ }
+}
+
+func compareByPackageVersion(a, b Match) int {
+ aVersion := a.Artifact.Version
+ bVersion := b.Artifact.Version
+
+ switch {
+ case aVersion < bVersion:
+ return -1
+ case aVersion > bVersion:
+ return 1
+ default:
+ return 0
+ }
+}
+
+func compareByPackageType(a, b Match) int {
+ aType := a.Artifact.Type
+ bType := b.Artifact.Type
+
+ switch {
+ case aType < bType:
+ return -1
+ case aType > bType:
+ return 1
+ default:
+ return 0
+ }
+}
+
+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 1
+ case "high":
+ return 2
+ case "medium":
+ return 3
+ case "low":
+ return 4
+ case "negligible":
+ return 5
+ default:
+ return 100 // least severe
+ }
+}
diff --git a/grype/presenter/models/sort_test.go b/grype/presenter/models/sort_test.go
new file mode 100644
index 00000000000..e86ae0c3a83
--- /dev/null
+++ b/grype/presenter/models/sort_test.go
@@ -0,0 +1,504 @@
+package models
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+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",
+ Version: "1.2.0",
+ Type: "npm",
+ },
+ },
+ {
+ // 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",
+ Version: "2.0.0",
+ Type: "docker",
+ },
+ },
+ {
+ // 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",
+ Version: "1.0.0",
+ Type: "npm",
+ },
+ },
+ {
+ // 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",
+ Version: "3.1.0",
+ Type: "gem",
+ },
+ },
+ {
+ // 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",
+ Version: "1.0.0",
+ Type: "docker",
+ },
+ },
+ }
+}
+
+func TestAllSortStrategies(t *testing.T) {
+ matches := createTestMatches()
+
+ 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)
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(string(tt.strategy), func(t *testing.T) {
+ testMatches := deepCopyMatches(matches)
+ SortMatches(testMatches, tt.strategy)
+
+ 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 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
+
+ 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("comparePackageAttributes", func(t *testing.T) {
+ result := comparePackageAttributes(m0, m1)
+ assert.Equal(t, 1, result, "package-b should come after package-a")
+
+ 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("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)
+
+ result := combined(m0, m1)
+ assert.Equal(t, 1, result, "medium should come after critical regardless of risk")
+
+ // 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
+
+ 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)
+ return result
+}
diff --git a/grype/presenter/models/source.go b/grype/presenter/models/source.go
index bdfecbe3c52..f21c0674f2f 100644
--- a/grype/presenter/models/source.go
+++ b/grype/presenter/models/source.go
@@ -3,6 +3,7 @@ package models
import (
"fmt"
+ "github.com/anchore/grype/grype/pkg"
syftSource "github.com/anchore/syft/syft/source"
)
@@ -14,7 +15,22 @@ 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 syftSource.StereoscopeImageSourceMetadata:
+ case pkg.SBOMFileMetadata:
+ return source{
+ Type: "sbom-file",
+ Target: m.Path,
+ }, nil
+ case pkg.PURLLiteralMetadata:
+ return source{
+ Type: "purl",
+ Target: m.PURL,
+ }, nil
+ case pkg.CPELiteralMetadata:
+ return source{
+ Type: "cpe",
+ Target: m.CPE,
+ }, nil
+ case syftSource.ImageMetadata:
// ensure that empty collections are not shown as null
if m.RepoDigests == nil {
m.RepoDigests = []string{}
@@ -27,12 +43,12 @@ func newSource(src syftSource.Description) (source, error) {
Type: "image",
Target: m,
}, nil
- case syftSource.DirectorySourceMetadata:
+ case syftSource.DirectoryMetadata:
return source{
Type: "directory",
Target: m.Path,
}, nil
- case syftSource.FileSourceMetadata:
+ case syftSource.FileMetadata:
return source{
Type: "file",
Target: m.Path,
diff --git a/grype/presenter/models/source_test.go b/grype/presenter/models/source_test.go
index fc24bef2b26..851fdf9a076 100644
--- a/grype/presenter/models/source_test.go
+++ b/grype/presenter/models/source_test.go
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/anchore/grype/grype/pkg"
syftSource "github.com/anchore/syft/syft/source"
)
@@ -18,7 +19,7 @@ func TestNewSource(t *testing.T) {
{
name: "image",
metadata: syftSource.Description{
- Metadata: syftSource.StereoscopeImageSourceMetadata{
+ Metadata: syftSource.ImageMetadata{
UserInput: "abc",
ID: "def",
ManifestDigest: "abcdef",
@@ -27,7 +28,7 @@ func TestNewSource(t *testing.T) {
},
expected: source{
Type: "image",
- Target: syftSource.StereoscopeImageSourceMetadata{
+ Target: syftSource.ImageMetadata{
UserInput: "abc",
ID: "def",
ManifestDigest: "abcdef",
@@ -40,7 +41,7 @@ func TestNewSource(t *testing.T) {
{
name: "directory",
metadata: syftSource.Description{
- Metadata: syftSource.DirectorySourceMetadata{
+ Metadata: syftSource.DirectoryMetadata{
Path: "/foo/bar",
},
},
@@ -52,7 +53,7 @@ func TestNewSource(t *testing.T) {
{
name: "file",
metadata: syftSource.Description{
- Metadata: syftSource.FileSourceMetadata{
+ Metadata: syftSource.FileMetadata{
Path: "/foo/bar/test.zip",
},
},
@@ -61,6 +62,52 @@ func TestNewSource(t *testing.T) {
Target: "/foo/bar/test.zip",
},
},
+ {
+ name: "purl-file",
+ metadata: syftSource.Description{
+ Metadata: pkg.SBOMFileMetadata{
+ Path: "/path/to/purls.txt",
+ },
+ },
+ expected: source{
+ Type: "sbom-file",
+ Target: "/path/to/purls.txt",
+ },
+ },
+ {
+ name: "purl-literal",
+ metadata: syftSource.Description{
+ Metadata: pkg.PURLLiteralMetadata{
+ PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
+ },
+ },
+ expected: source{
+ Type: "purl",
+ Target: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
+ },
+ },
+ {
+ name: "cpe-literal",
+ metadata: syftSource.Description{
+ Metadata: pkg.CPELiteralMetadata{
+ CPE: "cpe:/a:apache:log4j:2.14.1",
+ },
+ },
+ expected: source{
+ Type: "cpe",
+ Target: "cpe:/a:apache:log4j:2.14.1",
+ },
+ },
+ {
+ name: "nil metadata",
+ metadata: syftSource.Description{
+ Metadata: nil,
+ },
+ expected: source{
+ Type: "unknown",
+ Target: "unknown",
+ },
+ },
}
for _, testCase := range testCases {
diff --git a/grype/presenter/models/vulnerability.go b/grype/presenter/models/vulnerability.go
index c2745ce19cd..38216ce37cb 100644
--- a/grype/presenter/models/vulnerability.go
+++ b/grype/presenter/models/vulnerability.go
@@ -1,13 +1,18 @@
package models
import (
+ "sort"
+
+ "github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
)
type Vulnerability struct {
VulnerabilityMetadata
Fix Fix `json:"fix"`
Advisories []Advisory `json:"advisories"`
+ Risk float64 `json:"risk"`
}
type Fix struct {
@@ -20,7 +25,7 @@ type Advisory struct {
Link string `json:"link"`
}
-func NewVulnerability(vuln vulnerability.Vulnerability, metadata *vulnerability.Metadata) Vulnerability {
+func NewVulnerability(vuln vulnerability.Vulnerability, metadata *vulnerability.Metadata, versionFormat version.Format) Vulnerability {
if metadata == nil {
return Vulnerability{
VulnerabilityMetadata: NewVulnerabilityMetadata(vuln.ID, vuln.Namespace, metadata),
@@ -44,9 +49,46 @@ func NewVulnerability(vuln vulnerability.Vulnerability, metadata *vulnerability.
return Vulnerability{
VulnerabilityMetadata: NewVulnerabilityMetadata(vuln.ID, vuln.Namespace, metadata),
Fix: Fix{
- Versions: fixedInVersions,
+ Versions: sortVersions(fixedInVersions, versionFormat),
State: string(vuln.Fix.State),
},
Advisories: advisories,
+ Risk: metadata.RiskScore(),
+ }
+}
+func sortVersions(fixedVersions []string, format version.Format) []string {
+ if len(fixedVersions) <= 1 {
+ return fixedVersions
+ }
+
+ // First, create Version objects from strings (only once)
+ versionObjs := make([]*version.Version, 0, len(fixedVersions))
+ for _, vStr := range fixedVersions {
+ v, err := version.NewVersion(vStr, format)
+ if err != nil {
+ log.WithFields("version", vStr, "error", err).Trace("error parsing version, skipping")
+ continue
+ }
+ versionObjs = append(versionObjs, v)
}
+
+ // Sort the Version objects
+ sort.Slice(versionObjs, func(i, j int) bool {
+ // Compare returns -1 if v[i] < v[j], so we negate for descending order
+ // (higher versions first)
+ comparison, err := versionObjs[i].Compare(versionObjs[j])
+ if err != nil {
+ log.WithFields("error", err).Trace("error comparing versions")
+ return false
+ }
+ return comparison > 0 // Descending order
+ })
+
+ // Convert back to strings
+ result := make([]string, len(versionObjs))
+ for i, v := range versionObjs {
+ result[i] = v.Raw
+ }
+
+ return result
}
diff --git a/grype/presenter/models/vulnerability_metadata.go b/grype/presenter/models/vulnerability_metadata.go
index b7993fd5807..d02e6243227 100644
--- a/grype/presenter/models/vulnerability_metadata.go
+++ b/grype/presenter/models/vulnerability_metadata.go
@@ -1,15 +1,41 @@
package models
-import "github.com/anchore/grype/grype/vulnerability"
+import (
+ "time"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
type VulnerabilityMetadata struct {
- ID string `json:"id"`
- DataSource string `json:"dataSource"`
- Namespace string `json:"namespace,omitempty"`
- Severity string `json:"severity,omitempty"`
- URLs []string `json:"urls"`
- Description string `json:"description,omitempty"`
- Cvss []Cvss `json:"cvss"`
+ ID string `json:"id"`
+ DataSource string `json:"dataSource"`
+ Namespace string `json:"namespace,omitempty"`
+ Severity string `json:"severity,omitempty"`
+ URLs []string `json:"urls"`
+ Description string `json:"description,omitempty"`
+ Cvss []Cvss `json:"cvss"`
+ KnownExploited []KnownExploited `json:"knownExploited,omitempty"`
+ EPSS []EPSS `json:"epss,omitempty"`
+}
+
+type KnownExploited struct {
+ CVE string `json:"cve"`
+ VendorProject string `json:"vendorProject,omitempty"`
+ Product string `json:"product,omitempty"`
+ DateAdded string `json:"dateAdded,omitempty"`
+ RequiredAction string `json:"requiredAction,omitempty"`
+ DueDate string `json:"dueDate,omitempty"`
+ KnownRansomwareCampaignUse string `json:"knownRansomwareCampaignUse"`
+ Notes string `json:"notes,omitempty"`
+ URLs []string `json:"urls,omitempty"`
+ CWEs []string `json:"cwes,omitempty"`
+}
+
+type EPSS struct {
+ CVE string `json:"cve"`
+ EPSS float64 `json:"epss"`
+ Percentile float64 `json:"percentile"`
+ Date string `json:"date"`
}
func NewVulnerabilityMetadata(id, namespace string, metadata *vulnerability.Metadata) VulnerabilityMetadata {
@@ -26,12 +52,53 @@ func NewVulnerabilityMetadata(id, namespace string, metadata *vulnerability.Meta
}
return VulnerabilityMetadata{
- ID: id,
- DataSource: metadata.DataSource,
- Namespace: metadata.Namespace,
- Severity: metadata.Severity,
- URLs: urls,
- Description: metadata.Description,
- Cvss: NewCVSS(metadata),
+ ID: id,
+ DataSource: metadata.DataSource,
+ Namespace: metadata.Namespace,
+ Severity: metadata.Severity,
+ URLs: urls,
+ Description: metadata.Description,
+ Cvss: toCVSS(metadata),
+ KnownExploited: toKnownExploited(metadata.KnownExploited),
+ EPSS: toEPSS(metadata.EPSS),
+ }
+}
+
+func toKnownExploited(knownExploited []vulnerability.KnownExploited) []KnownExploited {
+ result := make([]KnownExploited, len(knownExploited))
+ for idx, ke := range knownExploited {
+ result[idx] = KnownExploited{
+ CVE: ke.CVE,
+ VendorProject: ke.VendorProject,
+ Product: ke.Product,
+ DateAdded: formatDate(ke.DateAdded),
+ RequiredAction: ke.RequiredAction,
+ DueDate: formatDate(ke.DueDate),
+ KnownRansomwareCampaignUse: ke.KnownRansomwareCampaignUse,
+ Notes: ke.Notes,
+ URLs: ke.URLs,
+ CWEs: ke.CWEs,
+ }
+ }
+ 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 {
+ result[idx] = EPSS{
+ CVE: e.CVE,
+ EPSS: e.EPSS,
+ Percentile: e.Percentile,
+ Date: e.Date.Format(time.DateOnly),
+ }
}
+ return result
}
diff --git a/grype/presenter/sarif/presenter.go b/grype/presenter/sarif/presenter.go
index 95eb7c2ff15..4bd6a3cc765 100644
--- a/grype/presenter/sarif/presenter.go
+++ b/grype/presenter/sarif/presenter.go
@@ -1,44 +1,43 @@
package sarif
import (
+ "crypto/sha256"
"fmt"
+ "hash"
"io"
"path/filepath"
+ "regexp"
"strings"
"github.com/owenrumney/go-sarif/sarif"
- v5 "github.com/anchore/grype/grype/db/v5"
- "github.com/anchore/grype/grype/match"
+ "github.com/anchore/clio"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/presenter/models"
"github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/grype/internal/version"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
)
// Presenter holds the data for generating a report and implements the presenter.Presenter interface
type Presenter struct {
- results match.Matches
- packages []pkg.Package
- src *source.Description
- metadataProvider vulnerability.MetadataProvider
+ id clio.Identification
+ document models.Document
+ src source.Description
}
-// NewPresenter is a *Presenter constructor
+// NewPresenter is a Presenter constructor
func NewPresenter(pb models.PresenterConfig) *Presenter {
return &Presenter{
- results: pb.Matches,
- packages: pb.Packages,
- metadataProvider: pb.MetadataProvider,
- src: pb.Context.Source,
+ id: pb.ID,
+ document: pb.Document,
+ src: pb.SBOM.Source,
}
}
// Present creates a SARIF-based report
-func (pres *Presenter) Present(output io.Writer) error {
- doc, err := pres.toSarifReport()
+func (p Presenter) Present(output io.Writer) error {
+ doc, err := p.toSarifReport()
if err != nil {
return err
}
@@ -47,14 +46,14 @@ func (pres *Presenter) Present(output io.Writer) error {
}
// toSarifReport outputs a sarif report object
-func (pres *Presenter) toSarifReport() (*sarif.Report, error) {
+func (p Presenter) toSarifReport() (*sarif.Report, error) {
doc, err := sarif.New(sarif.Version210)
if err != nil {
return nil, err
}
- v := version.FromBuild().Version
- if v == "[not provided]" {
+ v := p.id.Version
+ if v == "[not provided]" || v == "" {
// Need a semver to pass the MS SARIF validator
v = "0.0.0-dev"
}
@@ -62,25 +61,25 @@ func (pres *Presenter) toSarifReport() (*sarif.Report, error) {
doc.AddRun(&sarif.Run{
Tool: sarif.Tool{
Driver: &sarif.ToolComponent{
- Name: "Grype",
+ Name: p.id.Name,
Version: sp(v),
InformationURI: sp("https://github.com/anchore/grype"),
- Rules: pres.sarifRules(),
+ Rules: p.sarifRules(),
},
},
- Results: pres.sarifResults(),
+ Results: p.sarifResults(),
})
return doc, nil
}
// sarifRules generates the set of rules to include in this run
-func (pres *Presenter) sarifRules() (out []*sarif.ReportingDescriptor) {
- if pres.results.Count() > 0 {
+func (p Presenter) sarifRules() (out []*sarif.ReportingDescriptor) {
+ if len(p.document.Matches) > 0 {
ruleIDs := map[string]bool{}
- for _, m := range pres.results.Sorted() {
- ruleID := pres.ruleID(m)
+ for _, m := range p.document.Matches {
+ ruleID := p.ruleID(m)
if ruleIDs[ruleID] {
// here, we're only outputting information about the vulnerabilities, not where we matched them
continue
@@ -90,61 +89,64 @@ func (pres *Presenter) sarifRules() (out []*sarif.ReportingDescriptor) {
// Entirely possible to not have any links whatsoever
link := m.Vulnerability.ID
- meta := pres.metadata(m)
- if meta != nil {
- switch {
- case meta.DataSource != "":
- link = fmt.Sprintf("[%s](%s)", meta.ID, meta.DataSource)
- case len(meta.URLs) > 0:
- link = fmt.Sprintf("[%s](%s)", meta.ID, meta.URLs[0])
- }
+ switch {
+ case m.Vulnerability.DataSource != "":
+ link = fmt.Sprintf("[%s](%s)", m.Vulnerability.ID, m.Vulnerability.DataSource)
+ case len(m.Vulnerability.URLs) > 0:
+ link = fmt.Sprintf("[%s](%s)", m.Vulnerability.ID, m.Vulnerability.URLs[0])
}
- out = append(out, &sarif.ReportingDescriptor{
+ descriptor := sarif.ReportingDescriptor{
ID: ruleID,
Name: sp(ruleName(m)),
HelpURI: sp("https://github.com/anchore/grype"),
// Title of the SARIF report
ShortDescription: &sarif.MultiformatMessageString{
- Text: sp(pres.shortDescription(m)),
+ Text: sp(shortDescription(m)),
},
// Subtitle of the SARIF report
FullDescription: &sarif.MultiformatMessageString{
- Text: sp(pres.subtitle(m)),
+ Text: sp(subtitle(m)),
},
- Help: pres.helpText(m, link),
+ Help: p.helpText(m, link),
Properties: sarif.Properties{
// For GitHub reportingDescriptor object:
// https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#reportingdescriptor-object
- "security-severity": pres.securitySeverityValue(m),
+ "security-severity": securitySeverityValue(m),
},
- })
+ }
+
+ if len(m.Artifact.PURL) != 0 {
+ descriptor.Properties["purls"] = []string{m.Artifact.PURL}
+ }
+
+ out = append(out, &descriptor)
}
}
return out
}
// ruleID creates a unique rule ID for a given match
-func (pres *Presenter) ruleID(m match.Match) string {
+func (p Presenter) ruleID(m models.Match) string {
// TODO if we support configuration, we may want to allow addition of another qualifier such that if multiple
// vuln scans are run on multiple containers we can identify unique rules for each
- return fmt.Sprintf("%s-%s", m.Vulnerability.ID, m.Package.Name)
+ return fmt.Sprintf("%s-%s", m.Vulnerability.ID, m.Artifact.Name)
}
// helpText gets the help text for a rule, this is displayed in GitHub if you click on the title in a list of vulns
-func (pres *Presenter) helpText(m match.Match, link string) *sarif.MultiformatMessageString {
+func (p Presenter) helpText(m models.Match, link string) *sarif.MultiformatMessageString {
// TODO we shouldn't necessarily be adding a location here, there may be multiple referencing the same vulnerability
// we could instead add some list of all affected locations in the case there are a number found within an image,
// for example but this might get more complicated if there are multiple vuln scans for a particular branch
text := fmt.Sprintf("Vulnerability %s\nSeverity: %s\nPackage: %s\nVersion: %s\nFix Version: %s\nType: %s\nLocation: %s\nData Namespace: %s\nLink: %s",
- m.Vulnerability.ID, pres.severityText(m), m.Package.Name, m.Package.Version, fixVersions(m), m.Package.Type, pres.packagePath(m.Package), m.Vulnerability.Namespace, link,
+ m.Vulnerability.ID, severityText(m), m.Artifact.Name, m.Artifact.Version, fixVersions(m), m.Artifact.Type, p.packagePath(m.Artifact), m.Vulnerability.Namespace, link,
)
markdown := fmt.Sprintf(
"**Vulnerability %s**\n"+
"| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n"+
"| --- | --- | --- | --- | --- | --- | --- | --- |\n"+
"| %s | %s | %s | %s | %s | %s | %s | %s |\n",
- m.Vulnerability.ID, pres.severityText(m), m.Package.Name, m.Package.Version, fixVersions(m), m.Package.Type, pres.packagePath(m.Package), m.Vulnerability.Namespace, link,
+ m.Vulnerability.ID, severityText(m), m.Artifact.Name, m.Artifact.Version, fixVersions(m), m.Artifact.Type, p.packagePath(m.Artifact), m.Vulnerability.Namespace, link,
)
return &sarif.MultiformatMessageString{
Text: &text,
@@ -153,24 +155,20 @@ func (pres *Presenter) helpText(m match.Match, link string) *sarif.MultiformatMe
}
// packagePath attempts to get the relative path of the package to the "scan root"
-func (pres *Presenter) packagePath(p pkg.Package) string {
- locations := p.Locations.ToSlice()
- if len(locations) > 0 {
- return pres.locationPath(locations[0])
+func (p Presenter) packagePath(a models.Package) string {
+ if len(a.Locations) > 0 {
+ return p.locationPath(a.Locations[0])
}
- return pres.inputPath()
+ return p.inputPath()
}
// inputPath returns a friendlier relative path or absolute path depending on the input, not prefixed by . or ./
-func (pres *Presenter) inputPath() string {
- if pres.src == nil {
- return ""
- }
+func (p Presenter) inputPath() string {
var inputPath string
- switch m := pres.src.Metadata.(type) {
- case source.FileSourceMetadata:
+ switch m := p.src.Metadata.(type) {
+ case source.FileMetadata:
inputPath = m.Path
- case source.DirectorySourceMetadata:
+ case source.DirectoryMetadata:
inputPath = m.Path
default:
return ""
@@ -183,60 +181,56 @@ func (pres *Presenter) inputPath() string {
}
// locationPath returns a path for the location, relative to the cwd
-func (pres *Presenter) locationPath(l file.Location) string {
- path := l.RealPath
- if l.VirtualPath != "" {
- path = l.VirtualPath
- }
- in := pres.inputPath()
+func (p Presenter) locationPath(l file.Location) string {
+ path := l.Path()
+ in := p.inputPath()
path = strings.TrimPrefix(path, "./")
// trimmed off any ./ and accounted for dir:. for both path and input path
- if pres.src != nil {
- _, ok := pres.src.Metadata.(source.DirectorySourceMetadata)
- if ok {
- if filepath.IsAbs(path) || in == "" {
- return path
- }
- // return a path relative to the cwd, if it's not absolute
- return fmt.Sprintf("%s/%s", in, path)
+ _, ok := p.src.Metadata.(source.DirectoryMetadata)
+ if ok {
+ if filepath.IsAbs(path) || in == "" {
+ return path
}
+ // return a path relative to the cwd, if it's not absolute
+ return fmt.Sprintf("%s/%s", in, path)
}
return path
}
// locations the locations array is a single "physical" location with potentially multiple logical locations
-func (pres *Presenter) locations(m match.Match) []*sarif.Location {
- physicalLocation := pres.packagePath(m.Package)
+func (p Presenter) locations(m models.Match) []*sarif.Location {
+ physicalLocation := p.packagePath(m.Artifact)
var logicalLocations []*sarif.LogicalLocation
- switch metadata := pres.src.Metadata.(type) {
- case source.StereoscopeImageSourceMetadata:
+ switch metadata := p.src.Metadata.(type) {
+ case source.ImageMetadata:
img := metadata.UserInput
- locations := m.Package.Locations.ToSlice()
+ locations := m.Artifact.Locations
for _, l := range locations {
- trimmedPath := strings.TrimPrefix(pres.locationPath(l), "/")
+ trimmedPath := strings.TrimLeft(p.locationPath(l), "/")
logicalLocations = append(logicalLocations, &sarif.LogicalLocation{
FullyQualifiedName: sp(fmt.Sprintf("%s@%s:/%s", img, l.FileSystemID, trimmedPath)),
Name: sp(l.RealPath),
})
}
- // this is a hack to get results to show up in GitHub, as it requires relative paths for the location
- // but we really won't have any information about what Dockerfile on the filesystem was used to build the image
- // TODO we could add configuration to specify the prefix, a user might want to specify an image name and architecture
- // in the case of multiple vuln scans, for example
- physicalLocation = fmt.Sprintf("image/%s", physicalLocation)
- case source.FileSourceMetadata:
- locations := m.Package.Locations.ToSlice()
+ // GitHub requires paths for the location, but we really don't have any information about what
+ // file(s) these originated from in the repository. e.g. which Dockerfile was used to build an image,
+ // so we just use a short path-compatible image name here, not the entire user input as it may include
+ // sha and/or tags which are likely to change between runs and aren't really necessary for a general
+ // path to find file where the package originated
+ physicalLocation = fmt.Sprintf("%s/%s", imageShortPathName(p.src), physicalLocation)
+ case source.FileMetadata:
+ locations := m.Artifact.Locations
for _, l := range locations {
logicalLocations = append(logicalLocations, &sarif.LogicalLocation{
- FullyQualifiedName: sp(fmt.Sprintf("%s:/%s", metadata.Path, pres.locationPath(l))),
+ FullyQualifiedName: sp(fmt.Sprintf("%s:/%s", metadata.Path, p.locationPath(l))),
Name: sp(l.RealPath),
})
}
- case source.DirectorySourceMetadata:
+ case source.DirectoryMetadata:
// DirectoryScheme is already handled, with input prepended if needed
}
@@ -260,37 +254,27 @@ func (pres *Presenter) locations(m match.Match) []*sarif.Location {
}
// severityText provides a textual representation of the severity level of the match
-func (pres *Presenter) severityText(m match.Match) string {
- meta := pres.metadata(m)
- if meta != nil {
- severity := vulnerability.ParseSeverity(meta.Severity)
- switch severity {
- case vulnerability.CriticalSeverity:
- return "critical"
- case vulnerability.HighSeverity:
- return "high"
- case vulnerability.MediumSeverity:
- return "medium"
- }
+func severityText(m models.Match) string {
+ severity := vulnerability.ParseSeverity(m.Vulnerability.Severity)
+ switch severity {
+ case vulnerability.CriticalSeverity:
+ return "critical"
+ case vulnerability.HighSeverity:
+ return "high"
+ case vulnerability.MediumSeverity:
+ return "medium"
}
+
return "low"
}
// cvssScore attempts to get the best CVSS score that our vulnerability data contains
-func (pres *Presenter) cvssScore(v vulnerability.Vulnerability) float64 {
- var all []*vulnerability.Metadata
-
- meta, err := pres.metadataProvider.GetMetadata(v.ID, v.Namespace)
- if err == nil && meta != nil {
- all = append(all, meta)
+func cvssScore(m models.Match) float64 {
+ all := []models.VulnerabilityMetadata{
+ m.Vulnerability.VulnerabilityMetadata,
}
- for _, related := range v.RelatedVulnerabilities {
- meta, err = pres.metadataProvider.GetMetadata(related.ID, related.Namespace)
- if err == nil && meta != nil {
- all = append(all, meta)
- }
- }
+ all = append(all, m.RelatedVulnerabilities...)
score := -1.0
@@ -324,75 +308,80 @@ func (pres *Presenter) cvssScore(v vulnerability.Vulnerability) float64 {
// securitySeverityValue GitHub security-severity property uses a numeric severity value to determine whether things
// are critical, high, etc.; this converts our vulnerability to a value within the ranges
-func (pres *Presenter) securitySeverityValue(m match.Match) string {
- meta := pres.metadata(m)
- if meta != nil {
- // this corresponds directly to the CVSS score, so we return this if we have it
- score := pres.cvssScore(m.Vulnerability)
- if score > 0 {
- return fmt.Sprintf("%.1f", score)
- }
- severity := vulnerability.ParseSeverity(meta.Severity)
- switch severity {
- case vulnerability.CriticalSeverity:
- return "9.0"
- case vulnerability.HighSeverity:
- return "7.0"
- case vulnerability.MediumSeverity:
- return "4.0"
- case vulnerability.LowSeverity:
- return "1.0"
- }
+func securitySeverityValue(m models.Match) string {
+ // this corresponds directly to the CVSS score, so we return this if we have it
+ score := cvssScore(m)
+ if score > 0 {
+ return fmt.Sprintf("%.1f", score)
}
+ severity := vulnerability.ParseSeverity(m.Vulnerability.Severity)
+ switch severity {
+ case vulnerability.CriticalSeverity:
+ return "9.0"
+ case vulnerability.HighSeverity:
+ return "7.0"
+ case vulnerability.MediumSeverity:
+ return "4.0"
+ case vulnerability.LowSeverity:
+ return "1.0"
+ }
+
return "0.0"
}
-// metadata returns the matching *vulnerability.Metadata from the provider or nil if not found / error
-func (pres *Presenter) metadata(m match.Match) *vulnerability.Metadata {
- meta, _ := pres.metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
- return meta
+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 (pres *Presenter) subtitle(m match.Match) string {
- meta := pres.metadata(m)
- if meta != nil {
- subtitle := meta.Description
- if subtitle != "" {
- return subtitle
- }
+func subtitle(m models.Match) string {
+ subtitle := m.Vulnerability.Description
+ if subtitle != "" {
+ return subtitle
}
fixVersion := fixVersions(m)
if fixVersion != "" {
- return fmt.Sprintf("Version %s is affected with an available fix in versions %s", m.Package.Version, fixVersion)
+ return fmt.Sprintf("Version %s is affected with an available fix in versions %s", m.Artifact.Version, fixVersion)
}
- return fmt.Sprintf("Version %s is affected with no fixes reported yet.", m.Package.Version)
+ return fmt.Sprintf("Version %s is affected with no fixes reported yet.", m.Artifact.Version)
}
-func fixVersions(m match.Match) string {
- if m.Vulnerability.Fix.State == v5.FixedState && len(m.Vulnerability.Fix.Versions) > 0 {
+func fixVersions(m models.Match) string {
+ if m.Vulnerability.Fix.State == vulnerability.FixStateFixed.String() && len(m.Vulnerability.Fix.Versions) > 0 {
return strings.Join(m.Vulnerability.Fix.Versions, ",")
}
return ""
}
-func (pres *Presenter) shortDescription(m match.Match) string {
- return fmt.Sprintf("%s %s vulnerability for %s package", m.Vulnerability.ID, pres.severityText(m), m.Package.Name)
+func shortDescription(m models.Match) string {
+ return fmt.Sprintf("%s %s vulnerability for %s package", m.Vulnerability.ID, severityText(m), m.Artifact.Name)
}
-func (pres *Presenter) sarifResults() []*sarif.Result {
+func (p Presenter) sarifResults() []*sarif.Result {
out := make([]*sarif.Result, 0) // make sure we have at least an empty array
- for _, m := range pres.results.Sorted() {
+ for _, m := range p.document.Matches {
out = append(out, &sarif.Result{
- RuleID: sp(pres.ruleID(m)),
- Message: pres.resultMessage(m),
- // According to the SARIF spec, I believe we should be using AnalysisTarget.URI to indicate a logical
+ 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
- // FIXME github "requires" partialFingerprints
- // PartialFingerprints: ???
- Locations: pres.locations(m),
+ // GitHub requires partialFingerprints to upload to the API; these are automatically filled in
+ // when using the CodeQL upload action. See: https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#providing-data-to-track-code-scanning-alerts-across-runs
+ PartialFingerprints: p.partialFingerprints(m),
+ Locations: p.locations(m),
})
}
return out
@@ -408,26 +397,51 @@ func sp(sarif string) *string {
return &sarif
}
-func (pres *Presenter) resultMessage(m match.Match) sarif.Message {
- path := pres.packagePath(m.Package)
- message := fmt.Sprintf("The path %s reports %s at version %s ", path, m.Package.Name, m.Package.Version)
-
- if _, ok := pres.src.Metadata.(source.DirectorySourceMetadata); ok {
- message = fmt.Sprintf("%s which would result in a vulnerable (%s) package installed", message, m.Package.Type)
- } else {
- message = fmt.Sprintf("%s which is a vulnerable (%s) package installed in the container", message, m.Package.Type)
+func (p Presenter) resultMessage(m models.Match) sarif.Message {
+ path := p.packagePath(m.Artifact)
+ src := p.inputPath()
+ switch meta := p.src.Metadata.(type) {
+ case source.ImageMetadata:
+ src = fmt.Sprintf("in image %s at: %s", meta.UserInput, path)
+ case source.FileMetadata, source.DirectoryMetadata:
+ src = fmt.Sprintf("at: %s", path)
+ case pkg.PURLLiteralMetadata:
+ src = fmt.Sprintf("from purl literal %q", meta.PURL)
+ 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)
return sarif.Message{
Text: &message,
}
}
-func ruleName(m match.Match) string {
- if len(m.Details) > 0 {
- d := m.Details[0]
+func (p Presenter) partialFingerprints(m models.Match) map[string]any {
+ a := m.Artifact
+ hasher := sha256.New()
+ if meta, ok := p.src.Metadata.(source.ImageMetadata); ok {
+ hashWrite(hasher, p.src.Name, meta.Architecture, meta.OS)
+ }
+ hashWrite(hasher, string(a.Type), a.Name, a.Version, p.packagePath(a))
+ return map[string]any{
+ // this is meant to include :, but there isn't line information here, so just include :1
+ "primaryLocationLineHash": fmt.Sprintf("%x:1", hasher.Sum([]byte{})),
+ }
+}
+
+func hashWrite(hasher hash.Hash, values ...string) {
+ for _, value := range values {
+ _, _ = hasher.Write([]byte(value))
+ }
+}
+
+func ruleName(m models.Match) string {
+ if len(m.MatchDetails) > 0 {
+ d := m.MatchDetails[0]
buf := strings.Builder{}
- for _, segment := range []string{string(d.Matcher), string(d.Type)} {
+ for _, segment := range []string{d.Matcher, d.Type} {
for _, part := range strings.Split(segment, "-") {
buf.WriteString(strings.ToUpper(part[:1]))
buf.WriteString(part[1:])
@@ -437,3 +451,15 @@ func ruleName(m match.Match) string {
}
return m.Vulnerability.ID
}
+
+var nonPathChars = regexp.MustCompile("[^a-zA-Z0-9-_.]")
+
+// imageShortPathName returns path-compatible text describing the image. if the image name is the form
+// some/path/to/image, it will return the image portion of the name.
+func imageShortPathName(s source.Description) string {
+ imageName := s.Name
+ parts := strings.Split(imageName, "/")
+ imageName = parts[len(parts)-1]
+ imageName = nonPathChars.ReplaceAllString(imageName, "")
+ return imageName
+}
diff --git a/grype/presenter/sarif/presenter_test.go b/grype/presenter/sarif/presenter_test.go
index f770f5691a6..2b30cfec612 100644
--- a/grype/presenter/sarif/presenter_test.go
+++ b/grype/presenter/sarif/presenter_test.go
@@ -3,21 +3,23 @@ package sarif
import (
"bytes"
"flag"
- "fmt"
+ "os/exec"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"github.com/anchore/go-testutils"
- "github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/presenter/internal"
"github.com/anchore/grype/grype/presenter/models"
- "github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
+ "github.com/anchore/syft/syft/source/directorysource"
)
-var update = flag.Bool("update", 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) {
tests := []struct {
@@ -38,14 +40,8 @@ func TestSarifPresenter(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var buffer bytes.Buffer
- matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, tc.scheme)
- pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- Context: context,
- MetadataProvider: metadataProvider,
- }
+ pb := internal.GeneratePresenterConfig(t, tc.scheme)
pres := NewPresenter(pb)
err := pres.Present(&buffer)
@@ -54,7 +50,7 @@ func TestSarifPresenter(t *testing.T) {
}
actual := buffer.Bytes()
- if *update {
+ if *updateSnapshot {
testutils.UpdateGoldenFileContents(t, actual)
}
@@ -62,8 +58,51 @@ 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)
+ }
+ })
+ }
+}
+
+func Test_SarifIsValid(t *testing.T) {
+ tests := []struct {
+ name string
+ scheme internal.SyftSource
+ }{
+ {
+ name: "directory",
+ scheme: internal.DirectorySource,
+ },
+ {
+ name: "image",
+ scheme: internal.ImageSource,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var buffer bytes.Buffer
+
+ pb := internal.GeneratePresenterConfig(t, tc.scheme)
+
+ pres := NewPresenter(pb)
+ err := pres.Present(&buffer)
+ require.NoError(t, err)
+
+ cmd := exec.Command("docker", "run", "--rm", "-i", validatorImage)
+
+ out := bytes.Buffer{}
+ cmd.Stdout = &out
+ cmd.Stderr = &out
+
+ // pipe to the docker command
+ cmd.Stdin = &buffer
+
+ err = cmd.Run()
+ if err != nil || cmd.ProcessState.ExitCode() != 0 {
+ // valid
+ t.Fatalf("error validating SARIF document: %s", out.String())
}
})
}
@@ -79,7 +118,7 @@ func Test_locationPath(t *testing.T) {
}{
{
name: "dir:.",
- metadata: source.DirectorySourceMetadata{
+ metadata: source.DirectoryMetadata{
Path: ".",
},
real: "/home/usr/file",
@@ -88,7 +127,7 @@ func Test_locationPath(t *testing.T) {
},
{
name: "dir:./",
- metadata: source.DirectorySourceMetadata{
+ metadata: source.DirectoryMetadata{
Path: "./",
},
real: "/home/usr/file",
@@ -97,7 +136,7 @@ func Test_locationPath(t *testing.T) {
},
{
name: "dir:./someplace",
- metadata: source.DirectorySourceMetadata{
+ metadata: source.DirectoryMetadata{
Path: "./someplace",
},
real: "/home/usr/file",
@@ -106,7 +145,7 @@ func Test_locationPath(t *testing.T) {
},
{
name: "dir:/someplace",
- metadata: source.DirectorySourceMetadata{
+ metadata: source.DirectoryMetadata{
Path: "/someplace",
},
real: "file",
@@ -114,7 +153,7 @@ func Test_locationPath(t *testing.T) {
},
{
name: "dir:/someplace symlink",
- metadata: source.DirectorySourceMetadata{
+ metadata: source.DirectoryMetadata{
Path: "/someplace",
},
real: "/someplace/usr/file",
@@ -123,7 +162,7 @@ func Test_locationPath(t *testing.T) {
},
{
name: "dir:/someplace absolute",
- metadata: source.DirectorySourceMetadata{
+ metadata: source.DirectoryMetadata{
Path: "/someplace",
},
real: "/usr/file",
@@ -131,7 +170,7 @@ func Test_locationPath(t *testing.T) {
},
{
name: "file:/someplace/file",
- metadata: source.FileSourceMetadata{
+ metadata: source.FileMetadata{
Path: "/someplace/file",
},
real: "/usr/file",
@@ -139,7 +178,7 @@ func Test_locationPath(t *testing.T) {
},
{
name: "file:/someplace/file relative",
- metadata: source.FileSourceMetadata{
+ metadata: source.FileMetadata{
Path: "/someplace/file",
},
real: "file",
@@ -147,7 +186,7 @@ func Test_locationPath(t *testing.T) {
},
{
name: "image",
- metadata: source.StereoscopeImageSourceMetadata{
+ metadata: source.ImageMetadata{
UserInput: "alpine:latest",
},
real: "/etc/file",
@@ -155,7 +194,7 @@ func Test_locationPath(t *testing.T) {
},
{
name: "image symlink",
- metadata: source.StereoscopeImageSourceMetadata{
+ metadata: source.ImageMetadata{
UserInput: "alpine:latest",
},
real: "/etc/elsewhere/file",
@@ -167,14 +206,14 @@ func Test_locationPath(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pres := createDirPresenter(t)
- pres.src = &source.Description{
+ pres.src = source.Description{
Metadata: test.metadata,
}
- path := pres.packagePath(pkg.Package{
+ path := pres.packagePath(models.Package{
Locations: file.NewLocationSet(
file.NewVirtualLocation(test.real, test.virtual),
- ),
+ ).ToSlice(),
})
assert.Equal(t, test.expected, path)
@@ -183,22 +222,14 @@ func Test_locationPath(t *testing.T) {
}
func createDirPresenter(t *testing.T) *Presenter {
- matches, packages, _, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.DirectorySource)
d := t.TempDir()
- s, err := source.NewFromDirectory(source.DirectoryConfig{Path: d})
+ newSrc, err := directorysource.NewFromPath(d)
if err != nil {
t.Fatal(err)
}
- desc := s.Describe()
- pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- MetadataProvider: metadataProvider,
- Context: pkg.Context{
- Source: &desc,
- },
- }
+ pb := internal.GeneratePresenterConfig(t, internal.DirectorySource)
+ pb.SBOM.Source = newSrc.Describe()
pres := NewPresenter(pb)
@@ -223,8 +254,8 @@ func TestToSarifReport(t *testing.T) {
name: "image",
scheme: internal.ImageSource,
locations: map[string]string{
- "CVE-1999-0001-package-1": "image/somefile-1.txt",
- "CVE-1999-0002-package-2": "image/somefile-2.txt",
+ "CVE-1999-0001-package-1": "user-input/somefile-1.txt",
+ "CVE-1999-0002-package-2": "user-input/somefile-2.txt",
},
},
}
@@ -234,14 +265,7 @@ func TestToSarifReport(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
- matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, tc.scheme)
-
- pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- MetadataProvider: metadataProvider,
- Context: context,
- }
+ pb := internal.GeneratePresenterConfig(t, tc.scheme)
pres := NewPresenter(pb)
@@ -263,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]
@@ -273,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]
@@ -285,68 +311,58 @@ func TestToSarifReport(t *testing.T) {
}
-type NilMetadataProvider struct{}
-
-func (m *NilMetadataProvider) GetMetadata(_, _ string) (*vulnerability.Metadata, error) {
- return nil, nil
+func Test_cvssScoreWithMissingMetadata(t *testing.T) {
+ score := cvssScore(models.Match{
+ Vulnerability: models.Vulnerability{
+ VulnerabilityMetadata: models.VulnerabilityMetadata{
+ ID: "id",
+ Namespace: "namespace",
+ },
+ },
+ })
+ assert.Equal(t, float64(-1), score)
}
-type MockMetadataProvider struct{}
+func Test_cvssScore(t *testing.T) {
-func (m *MockMetadataProvider) GetMetadata(id, namespace string) (*vulnerability.Metadata, error) {
- cvss := func(id string, namespace string, scores ...float64) vulnerability.Metadata {
- values := make([]vulnerability.Cvss, len(scores))
+ cvss := func(id string, namespace string, scores ...float64) models.VulnerabilityMetadata {
+ values := make([]models.Cvss, 0, len(scores))
for _, score := range scores {
- values = append(values, vulnerability.Cvss{
- Metrics: vulnerability.CvssMetrics{
+ values = append(values, models.Cvss{
+ Metrics: models.CvssMetrics{
BaseScore: score,
},
})
}
- return vulnerability.Metadata{
+ return models.VulnerabilityMetadata{
ID: id,
Namespace: namespace,
Cvss: values,
}
}
- values := []vulnerability.Metadata{
- cvss("1", "nvd:cpe", 1),
- cvss("1", "not-nvd", 2),
- cvss("2", "not-nvd", 3, 4),
- }
- for _, v := range values {
- if v.ID == id && v.Namespace == namespace {
- return &v, nil
- }
- }
- return nil, fmt.Errorf("not found")
-}
-func Test_cvssScoreWithNilMetadata(t *testing.T) {
- pres := Presenter{
- metadataProvider: &NilMetadataProvider{},
- }
- score := pres.cvssScore(vulnerability.Vulnerability{
- ID: "id",
- Namespace: "namespace",
- })
- assert.Equal(t, float64(-1), score)
-}
+ nvd1 := cvss("1", "nvd:cpe", 1)
+ notNvd1 := cvss("1", "not-nvd", 2)
+ notNvd2 := cvss("2", "not-nvd", 3, 4)
-func Test_cvssScore(t *testing.T) {
tests := []struct {
- name string
- vulnerability vulnerability.Vulnerability
- expected float64
+ name string
+ match models.Match
+ expected float64
}{
{
name: "none",
- vulnerability: vulnerability.Vulnerability{
- ID: "4",
- RelatedVulnerabilities: []vulnerability.Reference{
+ match: models.Match{
+ Vulnerability: models.Vulnerability{
+ VulnerabilityMetadata: models.VulnerabilityMetadata{
+ ID: "4",
+ },
+ },
+ RelatedVulnerabilities: []models.VulnerabilityMetadata{
{
ID: "7",
Namespace: "nvd:cpe",
+ // intentionally missing info...
},
},
},
@@ -354,49 +370,45 @@ func Test_cvssScore(t *testing.T) {
},
{
name: "direct",
- vulnerability: vulnerability.Vulnerability{
- ID: "2",
- Namespace: "not-nvd",
- RelatedVulnerabilities: []vulnerability.Reference{
- {
- ID: "1",
- Namespace: "nvd:cpe",
- },
+ match: models.Match{
+ Vulnerability: models.Vulnerability{
+ VulnerabilityMetadata: notNvd2,
+ },
+ RelatedVulnerabilities: []models.VulnerabilityMetadata{
+ nvd1,
},
},
expected: 4,
},
{
name: "related not nvd",
- vulnerability: vulnerability.Vulnerability{
- ID: "1",
- Namespace: "nvd:cpe",
- RelatedVulnerabilities: []vulnerability.Reference{
- {
- ID: "1",
- Namespace: "nvd:cpe",
- },
- {
- ID: "1",
- Namespace: "not-nvd",
- },
+ match: models.Match{
+ Vulnerability: models.Vulnerability{
+ VulnerabilityMetadata: nvd1,
+ },
+ RelatedVulnerabilities: []models.VulnerabilityMetadata{
+ nvd1,
+ notNvd1,
},
},
expected: 2,
},
{
name: "related nvd",
- vulnerability: vulnerability.Vulnerability{
- ID: "4",
- Namespace: "not-nvd",
- RelatedVulnerabilities: []vulnerability.Reference{
- {
- ID: "1",
- Namespace: "nvd:cpe",
+ match: models.Match{
+ Vulnerability: models.Vulnerability{
+ VulnerabilityMetadata: models.VulnerabilityMetadata{
+ ID: "4",
+ Namespace: "not-nvd",
+ // intentionally missing info...
},
+ },
+ RelatedVulnerabilities: []models.VulnerabilityMetadata{
+ nvd1,
{
ID: "7",
Namespace: "not-nvd",
+ // intentionally missing info...
},
},
},
@@ -406,11 +418,45 @@ func Test_cvssScore(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- pres := Presenter{
- metadataProvider: &MockMetadataProvider{},
- }
- score := pres.cvssScore(test.vulnerability)
+ score := cvssScore(test.match)
assert.Equal(t, test.expected, score)
})
}
}
+
+func Test_imageShortPathName(t *testing.T) {
+ tests := []struct {
+ name string
+ in string
+ expected string
+ }{
+ {
+ name: "valid single name",
+ in: "simple.-_name",
+ expected: "simple.-_name",
+ },
+ {
+ name: "valid name in org",
+ in: "some-org/some-image",
+ expected: "some-image",
+ },
+ {
+ name: "name and org with many invalid chars",
+ in: "some/*^&$#%$#@*(}{<><./,valid-()(#)@!(~@#$#%^&**[]{-chars",
+ expected: "valid--chars",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got := imageShortPathName(
+ source.Description{
+ Name: test.in,
+ Metadata: nil,
+ },
+ )
+
+ assert.Equal(t, test.expected, got)
+ })
+ }
+}
diff --git a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden
index c8bb7eca8c4..91900ee484b 100644
--- a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden
+++ b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden
@@ -1,11 +1,11 @@
{
"version": "2.1.0",
- "$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json",
+ "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
"runs": [
{
"tool": {
"driver": {
- "name": "Grype",
+ "name": "grype",
"version": "0.0.0-dev",
"informationUri": "https://github.com/anchore/grype",
"rules": [
@@ -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: the-next-version\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 | the-next-version | 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,15 +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": {
- "security-severity": "1.0"
+ "purls": [
+ "pkg:deb/package-2@2.2.2"
+ ],
+ "security-severity": "8.5"
}
}
]
@@ -51,8 +54,9 @@
"results": [
{
"ruleId": "CVE-1999-0001-package-1",
+ "level": "note",
"message": {
- "text": "The path /some/path/somefile-1.txt reports package-1 at version 1.1.1 which would result in a vulnerable (rpm) package installed"
+ "text": "A low vulnerability in rpm package: package-1, version 1.1.1 was found at: /some/path/somefile-1.txt"
},
"locations": [
{
@@ -68,12 +72,16 @@
}
}
}
- ]
+ ],
+ "partialFingerprints": {
+ "primaryLocationLineHash": "0eefd3962fe456b80e5ddad4ec777c7f75b3c0586db887eff1c98f376fff60ba:1"
+ }
},
{
"ruleId": "CVE-1999-0002-package-2",
+ "level": "error",
"message": {
- "text": "The path /some/path/somefile-2.txt reports package-2 at version 2.2.2 which would result in a vulnerable (deb) package installed"
+ "text": "A critical vulnerability in deb package: package-2, version 2.2.2 was found at: /some/path/somefile-2.txt"
},
"locations": [
{
@@ -89,7 +97,10 @@
}
}
}
- ]
+ ],
+ "partialFingerprints": {
+ "primaryLocationLineHash": "0d4ef10dce50e71641e9314195020cea18febe4c6a4a8145a485154383d4fe0b:1"
+ }
}
]
}
diff --git a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden
index 488d7a9ab19..b9d33518d2b 100644
--- a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden
+++ b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden
@@ -1,11 +1,11 @@
{
"version": "2.1.0",
- "$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json",
+ "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
"runs": [
{
"tool": {
"driver": {
- "name": "Grype",
+ "name": "grype",
"version": "0.0.0-dev",
"informationUri": "https://github.com/anchore/grype",
"rules": [
@@ -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: the-next-version\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 | the-next-version | 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,15 +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": {
- "security-severity": "1.0"
+ "purls": [
+ "pkg:deb/package-2@2.2.2"
+ ],
+ "security-severity": "8.5"
}
}
]
@@ -51,14 +54,15 @@
"results": [
{
"ruleId": "CVE-1999-0001-package-1",
+ "level": "note",
"message": {
- "text": "The path somefile-1.txt reports package-1 at version 1.1.1 which is a vulnerable (rpm) package installed in the container"
+ "text": "A low vulnerability in rpm package: package-1, version 1.1.1 was found in image user-input at: somefile-1.txt"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
- "uri": "image/somefile-1.txt"
+ "uri": "user-input/somefile-1.txt"
},
"region": {
"startLine": 1,
@@ -74,18 +78,22 @@
}
]
}
- ]
+ ],
+ "partialFingerprints": {
+ "primaryLocationLineHash": "efe125c0a2b4bdafe476b69ba51a49734780c62b93803950319056acebe4323f:1"
+ }
},
{
"ruleId": "CVE-1999-0002-package-2",
+ "level": "error",
"message": {
- "text": "The path somefile-2.txt reports package-2 at version 2.2.2 which is a vulnerable (deb) package installed in the container"
+ "text": "A critical vulnerability in deb package: package-2, version 2.2.2 was found in image user-input at: somefile-2.txt"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
- "uri": "image/somefile-2.txt"
+ "uri": "user-input/somefile-2.txt"
},
"region": {
"startLine": 1,
@@ -101,7 +109,10 @@
}
]
}
- ]
+ ],
+ "partialFingerprints": {
+ "primaryLocationLineHash": "bafe9890c7cda00bf4d1b1a57d1d20b08e27162e718235a3d38a9a8d2f449ed1:1"
+ }
}
]
}
diff --git a/grype/presenter/table/__snapshots__/presenter_test.snap b/grype/presenter/table/__snapshots__/presenter_test.snap
new file mode 100755
index 00000000000..b3aa7fcbd1c
--- /dev/null
+++ b/grype/presenter/table/__snapshots__/presenter_test.snap
@@ -0,0 +1,53 @@
+
+[TestTablePresenter/no_color - 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
+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 EPSS% RISK
+package-1 1.1.1 1.2.1[38;5;240m, [0m[38;5;240m2.1.3[0m[38;5;240m, [0m[38;5;240m3.4.0[0m rpm CVE-1999-0001 [38;5;36mLow[0m 42.00 1.7
+package-2 2.2.2 deb CVE-1999-0002 [1;38;5;198mCritical[0m 53.00 96.3 [1;38;5;198m[0m[1;7;38;5;198mKEV[0m[1;38;5;198m[0m
+
+---
+
+[TestEmptyTablePresenter - 1]
+No vulnerabilities found
+
+---
+
+[TestHidesIgnoredMatches - 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
+package-2 2.2.2 deb CVE-1999-0002 Critical 53.00 96.3 (kev)
+
+---
+
+[TestDisplaysIgnoredMatches - 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
+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 e41dd46ec79..874a51a72c6 100644
--- a/grype/presenter/table/presenter.go
+++ b/grype/presenter/table/presenter.go
@@ -3,88 +3,104 @@ package table
import (
"fmt"
"io"
- "sort"
"strings"
+ "github.com/charmbracelet/lipgloss"
"github.com/olekukonko/tablewriter"
+ "github.com/scylladb/go-set/strset"
- grypeDb "github.com/anchore/grype/grype/db/v5"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
+ "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)"
+ appendSuppressed = "suppressed"
+ appendSuppressedVEX = "suppressed by VEX"
)
// Presenter is a generic struct for holding fields needed for reporting
type Presenter struct {
- results match.Matches
- ignoredMatches []match.IgnoredMatch
- packages []pkg.Package
- metadataProvider vulnerability.MetadataProvider
- showSuppressed bool
+ document models.Document
+ showSuppressed bool
+ 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
+
+type row struct {
+ Name string
+ Version string
+ Fix string
+ 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
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()
+ }
return &Presenter{
- results: pb.Matches,
- ignoredMatches: pb.IgnoredMatches,
- packages: pb.Packages,
- metadataProvider: pb.MetadataProvider,
- showSuppressed: showSuppressed,
+ 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
}
}
// Present creates a JSON-based reporting
-func (pres *Presenter) Present(output io.Writer) error {
- rows := make([][]string, 0)
-
- columns := []string{"Name", "Installed", "Fixed-In", "Type", "Vulnerability", "Severity"}
- // Generate rows for matching vulnerabilities
- for m := range pres.results.Enumerate() {
- row, err := createRow(m, pres.metadataProvider, "")
-
- if err != nil {
- return err
- }
- rows = append(rows, row)
- }
-
- // Generate rows for suppressed vulnerabilities
- if pres.showSuppressed {
- for _, m := range pres.ignoredMatches {
- row, err := createRow(m.Match, pres.metadataProvider, appendSuppressed)
+func (p *Presenter) Present(output io.Writer) error {
+ rs := p.getRows(p.document, p.showSuppressed)
- if err != nil {
- return err
- }
- rows = append(rows, row)
- }
- }
-
- if len(rows) == 0 {
+ if len(rs) == 0 {
_, err := io.WriteString(output, "No vulnerabilities found\n")
return err
}
- // sort by name, version, then type
- sort.SliceStable(rows, func(i, j int) bool {
- for col := 0; col < len(columns); col++ {
- if rows[i][col] != rows[j][col] {
- return rows[i][col] < rows[j][col]
- }
- }
- return false
- })
- rows = removeDuplicateRows(rows)
-
table := tablewriter.NewWriter(output)
-
- table.SetHeader(columns)
+ 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)
@@ -98,48 +114,269 @@ func (pres *Presenter) Present(output io.Writer) error {
table.SetTablePadding(" ")
table.SetNoWhiteSpace(true)
- table.AppendBulk(rows)
+ table.AppendBulk(rs.Render())
+
table.Render()
return nil
}
-func removeDuplicateRows(items [][]string) [][]string {
- seen := map[string][]string{}
- var result [][]string
+func (p *Presenter) getRows(doc models.Document, showSuppressed bool) rows {
+ var rs rows
- for _, v := range items {
- key := strings.Join(v, "|")
- if seen[key] != nil {
- // dup!
- continue
+ 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
+ }
}
+ }
- seen[key] = v
- result = append(result, v)
+ // generate rows for matching vulnerabilities
+ for _, m := range doc.Matches {
+ rs = append(rs, p.newRow(m, "", multipleDistros))
+ }
+
+ // generate rows for suppressed vulnerabilities
+ if showSuppressed {
+ for _, m := range doc.IgnoredMatches {
+ msg := appendSuppressed
+ if m.AppliedIgnoreRules != nil {
+ for i := range m.AppliedIgnoreRules {
+ if m.AppliedIgnoreRules[i].Namespace == "vex" {
+ msg = appendSuppressedVEX
+ }
+ }
+ }
+ rs = append(rs, p.newRow(m.Match, msg, multipleDistros))
+ }
+ }
+ return rs
+}
+
+func supportsColor() bool {
+ return lipgloss.NewStyle().Foreground(lipgloss.Color("5")).Render("") != ""
+}
+
+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{
+ Name: m.Artifact.Name,
+ Version: m.Artifact.Version,
+ Fix: p.formatFix(m),
+ PackageType: string(m.Artifact.Type),
+ VulnerabilityID: m.Vulnerability.ID,
+ 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,
}
- return result
}
-func createRow(m match.Match, metadataProvider vulnerability.MetadataProvider, severitySuffix string) ([]string, error) {
- var severity string
+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
+ }
- metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
- if err != nil {
- return nil, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
+ if severityStyle == nil {
+ severityStyle = &p.unknownStyle
}
- if metadata != nil {
- severity = metadata.Severity + severitySuffix
+ 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)
+}
- fixVersion := strings.Join(m.Vulnerability.Fix.Versions, ", ")
+func (p *Presenter) formatFix(m models.Match) string {
+ // adjust the model fix state values for better presentation
switch m.Vulnerability.Fix.State {
- case grypeDb.WontFixState:
- fixVersion = "(won't fix)"
- case grypeDb.UnknownFixState:
- fixVersion = ""
+ case vulnerability.FixStateWontFix.String():
+ return "(won't fix)"
+ case vulnerability.FixStateUnknown.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 {
+ continue
+ }
+ if d.Fix.SuggestedVersion != "" {
+ recommended.Add(d.Fix.SuggestedVersion)
+ }
+ }
+ return recommended
+}
+
+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 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)
+ }
+ }
+
+ 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 {
+ 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 {
+ return strings.Join(r.Columns(), "|")
+}
+
+func (rs rows) Render() [][]string {
+ deduped := rs.Deduplicate()
+ out := make([][]string, len(deduped))
+ for idx, r := range deduped {
+ out[idx] = r.Columns()
+ }
+ return out
+}
+
+func (rs rows) Deduplicate() []row {
+ // deduplicate
+ seen := map[string]row{}
+ var deduped rows
+
+ for _, v := range rs {
+ key := v.String()
+ if _, ok := seen[key]; ok {
+ // dup!
+ continue
+ }
+
+ seen[key] = v
+ deduped = append(deduped, v)
}
- return []string{m.Package.Name, m.Package.Version, fixVersion, string(m.Package.Type), m.Vulnerability.ID, severity}, nil
+ // render final columns
+ return deduped
}
diff --git a/grype/presenter/table/presenter_test.go b/grype/presenter/table/presenter_test.go
index ce44ad3b493..24bd168611a 100644
--- a/grype/presenter/table/presenter_test.go
+++ b/grype/presenter/table/presenter_test.go
@@ -2,14 +2,16 @@ package table
import (
"bytes"
- "flag"
"testing"
- "github.com/go-test/deep"
- "github.com/sergi/go-diff/diffmatchpatch"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/gkampitakis/go-snaps/snaps"
+ "github.com/google/go-cmp/cmp"
+ "github.com/muesli/termenv"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
- "github.com/anchore/go-testutils"
+ "github.com/anchore/clio"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/presenter/internal"
@@ -18,218 +20,332 @@ import (
syftPkg "github.com/anchore/syft/syft/pkg"
)
-var update = flag.Bool("update", false, "update the *.golden files for table presenters")
-
func TestCreateRow(t *testing.T) {
- pkg1 := pkg.Package{
+ pkg1 := models.Package{
ID: "package-1-id",
Name: "package-1",
- Version: "1.0.1",
+ Version: "2.0.0",
Type: syftPkg.DebPkg,
}
- match1 := match.Match{
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-1999-0001",
- Namespace: "source-1",
+ match1 := models.Match{
+ Vulnerability: models.Vulnerability{
+ Fix: models.Fix{
+ 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: "Medium",
+ Cvss: []models.Cvss{
+ {
+ Metrics: models.CvssMetrics{
+ BaseScore: 7,
+ },
+ 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,
+ },
+ },
+ },
},
- Package: pkg1,
- Details: []match.Detail{
+ Artifact: pkg1,
+ MatchDetails: []models.MatchDetails{
{
- Type: match.ExactDirectMatch,
- Matcher: match.DpkgMatcher,
+ Type: match.ExactDirectMatch.String(),
+ Matcher: match.DpkgMatcher.String(),
+ Fix: &models.FixDetails{
+ SuggestedVersion: "2.0.1",
+ },
},
},
}
+
+ matchWithKev := match1
+ matchWithKev.Vulnerability.KnownExploited = append(matchWithKev.Vulnerability.KnownExploited, models.KnownExploited{
+ CVE: "CVE-1999-0001",
+ KnownRansomwareCampaignUse: "Known",
+ })
+
cases := []struct {
- name string
- match match.Match
- severitySuffix string
- expectedErr error
- expectedRow []string
+ name string
+ match models.Match
+ extraAnnotation string
+ expectedRow []string
}{
{
- name: "create row for vulnerability",
- match: match1,
- severitySuffix: "",
- expectedErr: nil,
- expectedRow: []string{match1.Package.Name, match1.Package.Version, "", string(match1.Package.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,
+ 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",
- match: match1,
- severitySuffix: appendSuppressed,
- expectedErr: nil,
- expectedRow: []string{match1.Package.Name, match1.Package.Version, "", string(match1.Package.Type), match1.Vulnerability.ID, "Low (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) {
- row, err := createRow(testCase.match, models.NewMetadataMock(), testCase.severitySuffix)
+ p := NewPresenter(models.PresenterConfig{}, false)
+ row := p.newRow(testCase.match, testCase.extraAnnotation, false)
+ cols := rows{row}.Render()[0]
- assert.Equal(t, testCase.expectedErr, err)
- assert.Equal(t, testCase.expectedRow, row)
+ assert.Equal(t, testCase.expectedRow, cols)
})
}
}
func TestTablePresenter(t *testing.T) {
+ pb := internal.GeneratePresenterConfig(t, internal.ImageSource)
+
+ t.Run("no color", func(t *testing.T) {
+ var buffer bytes.Buffer
+ lipgloss.SetColorProfile(termenv.Ascii)
+ pres := NewPresenter(pb, false)
+
+ err := pres.Present(&buffer)
+ require.NoError(t, err)
+
+ actual := buffer.String()
+ snaps.MatchSnapshot(t, actual)
+ })
+
+ t.Run("with color", func(t *testing.T) {
+ var buffer bytes.Buffer
+ lipgloss.SetColorProfile(termenv.TrueColor)
+ t.Cleanup(func() {
+ // don't affect other tests
+ lipgloss.SetColorProfile(termenv.Ascii)
+ })
+ pres := NewPresenter(pb, false)
+
+ err := pres.Present(&buffer)
+ require.NoError(t, err)
+
+ actual := buffer.String()
+ snaps.MatchSnapshot(t, actual)
+ })
+}
+
+func TestEmptyTablePresenter(t *testing.T) {
+ // Expected to have no output
var buffer bytes.Buffer
- matches, packages, _, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.ImageSource)
+ doc, err := models.NewDocument(clio.Identification{}, nil, pkg.Context{}, match.NewMatches(), nil, nil, nil, nil, models.SortByPackage)
+ require.NoError(t, err)
pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- MetadataProvider: metadataProvider,
+ Document: doc,
}
pres := NewPresenter(pb, false)
// run presenter
- err := pres.Present(&buffer)
- if err != nil {
- t.Fatal(err)
- }
- actual := buffer.Bytes()
- if *update {
- testutils.UpdateGoldenFileContents(t, actual)
- }
+ err = pres.Present(&buffer)
+ require.NoError(t, err)
- var expected = testutils.GetGoldenFileContents(t)
-
- if !bytes.Equal(expected, actual) {
- dmp := diffmatchpatch.New()
- diffs := dmp.DiffMain(string(expected), string(actual), true)
- t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
- }
-
- // TODO: add me back in when there is a JSON schema
- // validateAgainstDbSchema(t, string(actual))
+ actual := buffer.String()
+ snaps.MatchSnapshot(t, actual)
}
-func TestEmptyTablePresenter(t *testing.T) {
- // Expected to have no output
-
+func TestHidesIgnoredMatches(t *testing.T) {
var buffer bytes.Buffer
- matches := match.NewMatches()
-
pb := models.PresenterConfig{
- Matches: matches,
- Packages: nil,
- MetadataProvider: nil,
+ Document: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource),
}
pres := NewPresenter(pb, false)
- // run presenter
err := pres.Present(&buffer)
- if err != nil {
- t.Fatal(err)
- }
- actual := buffer.Bytes()
- if *update {
- testutils.UpdateGoldenFileContents(t, actual)
- }
-
- var expected = testutils.GetGoldenFileContents(t)
-
- if !bytes.Equal(expected, actual) {
- dmp := diffmatchpatch.New()
- diffs := dmp.DiffMain(string(expected), string(actual), true)
- t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
- }
+ require.NoError(t, err)
+ actual := buffer.String()
+ snaps.MatchSnapshot(t, actual)
}
-func TestRemoveDuplicateRows(t *testing.T) {
- data := [][]string{
- {"1", "2", "3"},
- {"a", "b", "c"},
- {"1", "2", "3"},
- {"a", "b", "c"},
- {"1", "2", "3"},
- {"4", "5", "6"},
- {"1", "2", "1"},
- }
-
- expected := [][]string{
- {"1", "2", "3"},
- {"a", "b", "c"},
- {"4", "5", "6"},
- {"1", "2", "1"},
+func TestDisplaysIgnoredMatches(t *testing.T) {
+ var buffer bytes.Buffer
+ pb := models.PresenterConfig{
+ Document: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource),
}
- actual := removeDuplicateRows(data)
+ pres := NewPresenter(pb, true)
- if diffs := deep.Equal(expected, actual); len(diffs) > 0 {
- t.Errorf("found diffs!")
- for _, d := range diffs {
- t.Errorf(" diff: %+v", d)
- }
- }
+ err := pres.Present(&buffer)
+ require.NoError(t, err)
+ actual := buffer.String()
+ snaps.MatchSnapshot(t, actual)
}
-func TestHidesIgnoredMatches(t *testing.T) {
+func TestDisplaysDistro(t *testing.T) {
var buffer bytes.Buffer
- matches, ignoredMatches, packages, _, metadataProvider, _, _ := internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource)
-
pb := models.PresenterConfig{
- Matches: matches,
- IgnoredMatches: ignoredMatches,
- Packages: packages,
- MetadataProvider: metadataProvider,
+ 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)
- if err != nil {
- t.Fatal(err)
- }
- actual := buffer.Bytes()
- if *update {
- testutils.UpdateGoldenFileContents(t, actual)
- }
+ require.NoError(t, err)
- var expected = testutils.GetGoldenFileContents(t)
-
- if !bytes.Equal(expected, actual) {
- dmp := diffmatchpatch.New()
- diffs := dmp.DiffMain(string(expected), string(actual), true)
- t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
- }
+ actual := buffer.String()
+ snaps.MatchSnapshot(t, actual)
}
-func TestDisplaysIgnoredMatches(t *testing.T) {
+func TestDisplaysIgnoredMatchesAndDistro(t *testing.T) {
var buffer bytes.Buffer
- matches, ignoredMatches, packages, _, metadataProvider, _, _ := internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource)
-
pb := models.PresenterConfig{
- Matches: matches,
- IgnoredMatches: ignoredMatches,
- Packages: packages,
- MetadataProvider: metadataProvider,
+ 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)
- if err != nil {
- t.Fatal(err)
- }
- actual := buffer.Bytes()
- if *update {
- testutils.UpdateGoldenFileContents(t, actual)
+ 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) {
+ var rs rows
+ result := rs.Render()
+ assert.Empty(t, result)
+ })
+
+ t.Run("deduplicates identical rows", func(t *testing.T) {
+ rs := rows{
+ mustRow(t, "pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", vulnerability.FixStateFixed),
+ mustRow(t, "pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", vulnerability.FixStateFixed),
+ }
+ result := rs.Render()
+
+ expected := [][]string{
+ {"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)
+ }
+ })
+
+ t.Run("renders won't fix and empty fix versions correctly", func(t *testing.T) {
+ // Create rows with different fix states
+ row1 := mustRow(t, "pkgA", "1.0.0", "", "os", "CVE-2023-1234", "critical", vulnerability.FixStateUnknown)
+ row2 := mustRow(t, "pkgB", "2.0.0", "", "os", "CVE-2023-5678", "high", vulnerability.FixStateWontFix)
+ row3 := mustRow(t, "pkgC", "3.0.0", "3.1.0", "os", "CVE-2023-9012", "medium", vulnerability.FixStateFixed)
+
+ rs := rows{row1, row2, row3}
+ result := rs.Render()
+
+ expected := [][]string{
+ {"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 != "" {
+ t.Errorf("Render() mismatch (-want +got):\n%s", diff)
+ }
+ })
+
+ t.Run("column count matches expectations", func(t *testing.T) {
+ rs := rows{
+ mustRow(t, "pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", vulnerability.FixStateFixed),
+ }
+ result := rs.Render()
+
+ expected := [][]string{
+ {"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)
+ }
+
+ // expected columns: name, version, fix, packageType, vulnID, severity, epss, risk
+ assert.Len(t, result[0], 8)
+
+ })
+}
+
+func createTestRow(name, version, fix, pkgType, vulnID, severity string, fixState vulnerability.FixState) (row, error) {
+ m := models.Match{
+ Vulnerability: models.Vulnerability{
+ Fix: models.Fix{
+ Versions: []string{fix},
+ State: fixState.String(),
+ },
+ 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{
+ Name: name,
+ Version: version,
+ Type: syftPkg.Type(pkgType),
+ },
}
- var expected = testutils.GetGoldenFileContents(t)
+ p := NewPresenter(models.PresenterConfig{}, false)
+ r := p.newRow(m, "", false)
- if !bytes.Equal(expected, actual) {
- dmp := diffmatchpatch.New()
- diffs := dmp.DiffMain(string(expected), string(actual), true)
- t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
+ return r, nil
+}
+
+func mustRow(t *testing.T, name, version, fix, pkgType, vulnID, severity string, fixState vulnerability.FixState) row {
+ r, err := createTestRow(name, version, fix, pkgType, vulnID, severity, fixState)
+ if err != nil {
+ t.Fatalf("failed to create test row: %v", err)
}
+ return r
}
diff --git a/grype/presenter/table/test-fixtures/snapshot/TestDisplaysIgnoredMatches.golden b/grype/presenter/table/test-fixtures/snapshot/TestDisplaysIgnoredMatches.golden
deleted file mode 100644
index b74a500d50a..00000000000
--- a/grype/presenter/table/test-fixtures/snapshot/TestDisplaysIgnoredMatches.golden
+++ /dev/null
@@ -1,5 +0,0 @@
-NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
-package-1 1.1.1 rpm CVE-1999-0002 Critical
-package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low
-package-2 2.2.2 deb CVE-1999-0001 Low (suppressed)
-package-2 2.2.2 deb CVE-1999-0002 Critical (suppressed)
diff --git a/grype/presenter/table/test-fixtures/snapshot/TestEmptyTablePresenter.golden b/grype/presenter/table/test-fixtures/snapshot/TestEmptyTablePresenter.golden
deleted file mode 100644
index 8900c02cd74..00000000000
--- a/grype/presenter/table/test-fixtures/snapshot/TestEmptyTablePresenter.golden
+++ /dev/null
@@ -1 +0,0 @@
-No vulnerabilities found
diff --git a/grype/presenter/table/test-fixtures/snapshot/TestHidesIgnoredMatches.golden b/grype/presenter/table/test-fixtures/snapshot/TestHidesIgnoredMatches.golden
deleted file mode 100644
index 4d13482c154..00000000000
--- a/grype/presenter/table/test-fixtures/snapshot/TestHidesIgnoredMatches.golden
+++ /dev/null
@@ -1,3 +0,0 @@
-NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
-package-1 1.1.1 rpm CVE-1999-0002 Critical
-package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low
diff --git a/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden b/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden
deleted file mode 100644
index e16b5919029..00000000000
--- a/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden
+++ /dev/null
@@ -1,3 +0,0 @@
-NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
-package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low
-package-2 2.2.2 deb CVE-1999-0002 Critical
diff --git a/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter_Color.golden b/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter_Color.golden
new file mode 100644
index 00000000000..2d492005cf1
--- /dev/null
+++ b/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter_Color.golden
@@ -0,0 +1,3 @@
+NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
+package-1 1.1.1 the-next-version rpm CVE-1999-0001 [0;32mLow[0m
+package-2 2.2.2 deb CVE-1999-0002 [1;31mCritical[0m
diff --git a/grype/presenter/template/presenter.go b/grype/presenter/template/presenter.go
index 710e2851add..402b92017c5 100644
--- a/grype/presenter/template/presenter.go
+++ b/grype/presenter/template/presenter.go
@@ -5,40 +5,27 @@ import (
"io"
"os"
"reflect"
- "sort"
"text/template"
"github.com/Masterminds/sprig/v3"
- "github.com/mitchellh/go-homedir"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/clio"
+ "github.com/anchore/go-homedir"
"github.com/anchore/grype/grype/presenter/models"
- "github.com/anchore/grype/grype/vulnerability"
)
// Presenter is an implementation of presenter.Presenter that formats output according to a user-provided Go text template.
type Presenter struct {
- matches match.Matches
- ignoredMatches []match.IgnoredMatch
- packages []pkg.Package
- context pkg.Context
- metadataProvider vulnerability.MetadataProvider
- appConfig interface{}
- dbStatus interface{}
+ id clio.Identification
+ document models.Document
pathToTemplateFile string
}
// NewPresenter returns a new template.Presenter.
func NewPresenter(pb models.PresenterConfig, templateFile string) *Presenter {
return &Presenter{
- matches: pb.Matches,
- ignoredMatches: pb.IgnoredMatches,
- packages: pb.Packages,
- metadataProvider: pb.MetadataProvider,
- context: pb.Context,
- appConfig: pb.AppConfig,
- dbStatus: pb.DBStatus,
+ id: pb.ID,
+ document: pb.Document,
pathToTemplateFile: templateFile,
}
}
@@ -61,13 +48,7 @@ func (pres *Presenter) Present(output io.Writer) error {
return fmt.Errorf("unable to parse template: %w", err)
}
- document, err := models.NewDocument(pres.packages, pres.context, pres.matches, pres.ignoredMatches, pres.metadataProvider,
- pres.appConfig, pres.dbStatus)
- if err != nil {
- return err
- }
-
- err = tmpl.Execute(output, document)
+ err = tmpl.Execute(output, pres.document)
if err != nil {
return fmt.Errorf("unable to execute supplied template: %w", err)
}
@@ -91,7 +72,7 @@ var FuncMap = func() template.FuncMap {
return collection
}
- sort.Sort(models.ByName(matches))
+ models.SortMatches(matches, models.SortByPackage)
return matches
}
return f
diff --git a/grype/presenter/template/presenter_test.go b/grype/presenter/template/presenter_test.go
index 502f8ca3f75..06b8a15a65c 100644
--- a/grype/presenter/template/presenter_test.go
+++ b/grype/presenter/template/presenter_test.go
@@ -12,28 +12,18 @@ import (
"github.com/anchore/go-testutils"
"github.com/anchore/grype/grype/presenter/internal"
- "github.com/anchore/grype/grype/presenter/models"
)
var update = flag.Bool("update", false, "update the *.golden files for template presenters")
func TestPresenter_Present(t *testing.T) {
- matches, packages, context, metadataProvider, appConfig, dbStatus := internal.GenerateAnalysis(t, internal.ImageSource)
-
workingDirectory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
templateFilePath := path.Join(workingDirectory, "./test-fixtures/test.template")
- pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- Context: context,
- MetadataProvider: metadataProvider,
- AppConfig: appConfig,
- DBStatus: dbStatus,
- }
+ pb := internal.GeneratePresenterConfig(t, internal.ImageSource)
templatePresenter := NewPresenter(pb, templateFilePath)
@@ -53,21 +43,13 @@ func TestPresenter_Present(t *testing.T) {
}
func TestPresenter_SprigDate_Fails(t *testing.T) {
- matches, packages, context, metadataProvider, appConfig, dbStatus := internal.GenerateAnalysis(t, internal.ImageSource)
workingDirectory, err := os.Getwd()
require.NoError(t, err)
// this template has the generic sprig date function, which is intentionally not supported for security reasons
templateFilePath := path.Join(workingDirectory, "./test-fixtures/test.template.sprig.date")
- pb := models.PresenterConfig{
- Matches: matches,
- Packages: packages,
- Context: context,
- MetadataProvider: metadataProvider,
- AppConfig: appConfig,
- DBStatus: dbStatus,
- }
+ pb := internal.GeneratePresenterConfig(t, internal.ImageSource)
templatePresenter := NewPresenter(pb, templateFilePath)
diff --git a/grype/presenter/template/test-fixtures/snapshot/TestPresenter_Present.golden b/grype/presenter/template/test-fixtures/snapshot/TestPresenter_Present.golden
index 0ac37fa30dc..6ac980058c4 100644
--- a/grype/presenter/template/test-fixtures/snapshot/TestPresenter_Present.golden
+++ b/grype/presenter/template/test-fixtures/snapshot/TestPresenter_Present.golden
@@ -2,11 +2,11 @@ Identified distro as centos version 8.0.
Vulnerability: CVE-1999-0001
Severity: Low
Package: package-1 version 1.1.1 (rpm)
- CPEs: ["cpe:2.3:a:anchore:engine:0.9.2:*:*:python:*:*:*:*"]
+ CPEs: ["cpe:2.3:a:anchore:engine:0.9.2:*:*:en:*:*:*:*"]
Matched by: dpkg-matcher
Vulnerability: CVE-1999-0002
Severity: Critical
Package: package-2 version 2.2.2 (deb)
- CPEs: ["cpe:2.3:a:anchore:engine:2.2.2:*:*:python:*:*:*:*"]
+ CPEs: ["cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*"]
Matched by: dpkg-matcher
diff --git a/grype/search/cpe.go b/grype/search/cpe.go
index d9d97dd0b6a..7d6f9d684c1 100644
--- a/grype/search/cpe.go
+++ b/grype/search/cpe.go
@@ -2,250 +2,63 @@ package search
import (
"fmt"
- "sort"
"strings"
- "github.com/facebookincubator/nvdtools/wfn"
- "github.com/scylladb/go-set/strset"
-
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
- "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"
)
-type CPEPackageParameter struct {
- Name string `json:"name"`
- Version string `json:"version"`
-}
+var _ interface {
+ vulnerability.Criteria
+} = (*CPECriteria)(nil)
-type CPEParameters struct {
- Namespace string `json:"namespace"`
- CPEs []string `json:"cpes"`
- Package CPEPackageParameter
+type CPECriteria struct {
+ CPE cpe.CPE
}
-func (i *CPEParameters) Merge(other CPEParameters) error {
- if i.Namespace != other.Namespace {
- return fmt.Errorf("namespaces do not match")
+// ByCPE returns criteria which will search based on any of the provided CPEs
+func ByCPE(c cpe.CPE) vulnerability.Criteria {
+ return &CPECriteria{
+ CPE: c,
}
-
- existingCPEs := strset.New(i.CPEs...)
- newCPEs := strset.New(other.CPEs...)
- mergedCPEs := strset.Union(existingCPEs, newCPEs).List()
- sort.Strings(mergedCPEs)
- i.CPEs = mergedCPEs
- return nil
-}
-
-type CPEResult struct {
- VulnerabilityID string `json:"vulnerabilityID"`
- VersionConstraint string `json:"versionConstraint"`
- CPEs []string `json:"cpes"`
}
-func (h CPEResult) Equals(other CPEResult) bool {
- if h.VersionConstraint != other.VersionConstraint {
- return false
- }
-
- if len(h.CPEs) != len(other.CPEs) {
- return false
+func (v *CPECriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, string, error) {
+ if containsCPE(vuln.CPEs, v.CPE) {
+ return true, "", nil
}
-
- for i := range h.CPEs {
- if h.CPEs[i] != other.CPEs[i] {
- return false
- }
- }
-
- return true
+ return false, "CPE attributes do not match", nil
}
-func alpineCPEComparableVersion(version string) string {
- // clean the alpine package version so that it compares correctly with the CPE version comparison logic
- // alpine versions are suffixed with -r{buildindex}; however, if left intact CPE comparison logic will
- // incorrectly treat these as a pre-release. In actuality, we just want to treat 1.2.3-r21 as equivalent to
- // 1.2.3 for purposes of CPE-based matching since the alpine fix should filter out any cases where a later
- // build fixes something that was vulnerable in 1.2.3
- components := strings.Split(version, "-r")
- cpeComparableVersion := version
-
- if len(components) == 2 {
- cpeComparableVersion = components[0]
- }
-
- return cpeComparableVersion
+func (v *CPECriteria) Summarize() string {
+ return fmt.Sprintf("does not match CPE: %s", v.CPE.Attributes.BindToFmtString())
}
-// ByPackageCPE retrieves all vulnerabilities that match the generated CPE
-func ByPackageCPE(store vulnerability.ProviderByCPE, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) {
- // we attempt to merge match details within the same matcher when searching by CPEs, in this way there are fewer duplicated match
- // objects (and fewer duplicated match details).
- matchesByFingerprint := make(map[match.Fingerprint]match.Match)
- for _, c := range p.CPEs {
- // prefer the CPE version, but if npt specified use the package version
- searchVersion := c.Version
-
- if p.Type == syftPkg.ApkPkg {
- searchVersion = alpineCPEComparableVersion(searchVersion)
- }
-
- if searchVersion == wfn.NA || searchVersion == wfn.Any {
- searchVersion = p.Version
- }
- verObj, err := version.NewVersion(searchVersion, version.FormatFromPkgType(p.Type))
- 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)
- allPkgVulns, err := store.GetByCPE(c)
- if err != nil {
- return nil, fmt.Errorf("matcher failed to fetch by CPE pkg=%q: %w", p.Name, err)
- }
-
- applicableVulns, err := onlyQualifiedPackages(d, p, allPkgVulns)
- if err != nil {
- return nil, fmt.Errorf("unable to filter cpe-related vulnerabilities: %w", err)
- }
-
- // TODO: Port this over to a qualifier and remove
- applicableVulns, err = onlyVulnerableVersions(verObj, applicableVulns)
- if err != nil {
- return nil, fmt.Errorf("unable to filter cpe-related vulnerabilities: %w", err)
- }
-
- applicableVulns = onlyVulnerableTargets(p, applicableVulns)
-
- // for each vulnerability record found, check the version constraint. If the constraint is satisfied
- // relative to the current version information from the CPE (or the package) then the given package
- // is vulnerable.
- for _, vuln := range applicableVulns {
- addNewMatch(matchesByFingerprint, vuln, p, *verObj, upstreamMatcher, c)
+// containsCPE returns true if the provided slice contains a matching CPE based on attributes matching
+func containsCPE(cpes []cpe.CPE, cpe cpe.CPE) bool {
+ for _, c := range cpes {
+ if matchesAttributes(cpe.Attributes, c.Attributes) {
+ return true
}
}
-
- return toMatches(matchesByFingerprint), nil
+ return false
}
-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,
- Package: p,
- }
-
- if existingMatch, exists := matchesByFingerprint[candidateMatch.Fingerprint()]; exists {
- candidateMatch = existingMatch
- }
-
- candidateMatch.Details = addMatchDetails(candidateMatch.Details,
- match.Detail{
- Type: match.CPEMatch,
- Confidence: 0.9, // TODO: this is hard coded for now
- Matcher: upstreamMatcher,
- SearchedBy: CPEParameters{
- Namespace: vuln.Namespace,
- CPEs: []string{
- searchedByCPE.BindToFmtString(),
- },
- Package: CPEPackageParameter{
- Name: p.Name,
- Version: p.Version,
- },
- },
- Found: CPEResult{
- VulnerabilityID: vuln.ID,
- VersionConstraint: vuln.Constraint.String(),
- CPEs: cpesToString(filterCPEsByVersion(searchVersion, vuln.CPEs)),
- },
- },
- )
-
- matchesByFingerprint[candidateMatch.Fingerprint()] = candidateMatch
-}
-
-func addMatchDetails(existingDetails []match.Detail, newDetails match.Detail) []match.Detail {
- newFound, ok := newDetails.Found.(CPEResult)
- if !ok {
- return existingDetails
- }
-
- newSearchedBy, ok := newDetails.SearchedBy.(CPEParameters)
- if !ok {
- return existingDetails
- }
- for idx, detail := range existingDetails {
- found, ok := detail.Found.(CPEResult)
- if !ok {
- continue
- }
-
- searchedBy, ok := detail.SearchedBy.(CPEParameters)
- if !ok {
- continue
- }
-
- if !found.Equals(newFound) {
- continue
- }
-
- err := searchedBy.Merge(newSearchedBy)
- if err != nil {
- continue
- }
-
- existingDetails[idx].SearchedBy = searchedBy
- return existingDetails
- }
-
- // could not merge with another entry, append to the end
- existingDetails = append(existingDetails, newDetails)
- return existingDetails
-}
-
-func filterCPEsByVersion(pkgVersion version.Version, allCPEs []cpe.CPE) (matchedCPEs []cpe.CPE) {
- for _, c := range allCPEs {
- if c.Version == wfn.Any || c.Version == wfn.NA {
- matchedCPEs = append(matchedCPEs, c)
- continue
- }
-
- constraint, err := version.GetConstraint(c.Version, version.UnknownFormat)
- if err != nil {
- // if we can't get a version constraint, don't filter out the CPE
- matchedCPEs = append(matchedCPEs, c)
- continue
- }
-
- 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)
- continue
- }
- }
- return matchedCPEs
-}
-
-func toMatches(matchesByFingerprint map[match.Fingerprint]match.Match) (matches []match.Match) {
- for _, m := range matchesByFingerprint {
- matches = append(matches, m)
+func matchesAttributes(a1 cpe.Attributes, a2 cpe.Attributes) bool {
+ if !matchesAttribute(a1.Product, a2.Product) ||
+ !matchesAttribute(a1.Vendor, a2.Vendor) ||
+ !matchesAttribute(a1.Part, a2.Part) ||
+ !matchesAttribute(a1.Language, a2.Language) ||
+ !matchesAttribute(a1.SWEdition, a2.SWEdition) ||
+ !matchesAttribute(a1.TargetSW, a2.TargetSW) ||
+ !matchesAttribute(a1.TargetHW, a2.TargetHW) ||
+ !matchesAttribute(a1.Other, a2.Other) ||
+ !matchesAttribute(a1.Edition, a2.Edition) {
+ return false
}
- sort.Sort(match.ByElements(matches))
- return matches
+ return true
}
-// cpesToString receives one or more CPEs and stringifies them
-func cpesToString(cpes []cpe.CPE) []string {
- var strs = make([]string, len(cpes))
- for idx, c := range cpes {
- strs[idx] = c.BindToFmtString()
- }
- sort.Strings(strs)
- return strs
+func matchesAttribute(a1, a2 string) bool {
+ return a1 == "" || a2 == "" || strings.EqualFold(a1, a2)
}
diff --git a/grype/search/cpe_test.go b/grype/search/cpe_test.go
index e09591f4f2d..2d9b6f0907e 100644
--- a/grype/search/cpe_test.go
+++ b/grype/search/cpe_test.go
@@ -3,1110 +3,52 @@ package search
import (
"testing"
- "github.com/google/go-cmp/cmp"
- "github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/anchore/grype/grype/db"
- grypeDB "github.com/anchore/grype/grype/db/v5"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
- "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"
)
-var _ grypeDB.VulnerabilityStoreReader = (*mockVulnStore)(nil)
-
-type mockVulnStore struct {
- data map[string]map[string][]grypeDB.Vulnerability
-}
-
-func (pr *mockVulnStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
- //TODO implement me
- panic("implement me")
-}
-
-func newMockStore() *mockVulnStore {
- pr := mockVulnStore{
- data: make(map[string]map[string][]grypeDB.Vulnerability),
- }
- pr.stub()
- return &pr
-}
-
-func (pr *mockVulnStore) stub() {
- pr.data["nvd:cpe"] = map[string][]grypeDB.Vulnerability{
- "activerecord": {
- {
- PackageName: "activerecord",
- VersionConstraint: "< 3.7.6",
- VersionFormat: version.SemanticFormat.String(),
- ID: "CVE-2017-fake-1",
- CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
- },
- Namespace: "nvd:cpe",
- },
- {
- PackageName: "activerecord",
- VersionConstraint: "< 3.7.4",
- VersionFormat: version.SemanticFormat.String(),
- ID: "CVE-2017-fake-2",
- CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*",
- },
- Namespace: "nvd:cpe",
- },
- {
- PackageName: "activerecord",
- VersionConstraint: "= 4.0.1",
- VersionFormat: version.GemFormat.String(),
- ID: "CVE-2017-fake-3",
- CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*",
- },
- Namespace: "nvd:cpe",
- },
- },
- "awesome": {
- {
- PackageName: "awesome",
- VersionConstraint: "< 98SP3",
- VersionFormat: version.UnknownFormat.String(),
- ID: "CVE-2017-fake-4",
- CPEs: []string{
- "cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*",
- },
- Namespace: "nvd:cpe",
- },
- },
- "multiple": {
- {
- PackageName: "multiple",
- VersionConstraint: "< 4.0",
- VersionFormat: version.UnknownFormat.String(),
- ID: "CVE-2017-fake-5",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- "cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*",
- "cpe:2.3:*:multiple:multiple:3.0:*:*:*:*:*:*:*",
- },
- Namespace: "nvd:cpe",
- },
- },
- "funfun": {
- {
- PackageName: "funfun",
- VersionConstraint: "= 5.2.1",
- VersionFormat: version.PythonFormat.String(),
- ID: "CVE-2017-fake-6",
- CPEs: []string{
- "cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*",
- "cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*",
- },
- Namespace: "nvd:cpe",
- },
- },
- "sw": {
- {
- PackageName: "sw",
- VersionConstraint: "< 1.0",
- VersionFormat: version.UnknownFormat.String(),
- ID: "CVE-2017-fake-7",
- CPEs: []string{
- "cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*",
- },
- Namespace: "nvd:cpe",
- },
- },
- "handlebars": {
- {
- PackageName: "handlebars",
- VersionConstraint: "< 4.7.7",
- VersionFormat: version.UnknownFormat.String(),
- ID: "CVE-2021-23369",
- CPEs: []string{
- "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
- },
- Namespace: "nvd:cpe",
- },
- },
- }
-}
-
-func (pr *mockVulnStore) SearchForVulnerabilities(namespace, pkg string) ([]grypeDB.Vulnerability, error) {
- return pr.data[namespace][pkg], nil
-}
-
-func (pr *mockVulnStore) GetAllVulnerabilities() (*[]grypeDB.Vulnerability, error) {
- return nil, nil
-}
-
-func (pr *mockVulnStore) GetVulnerabilityNamespaces() ([]string, error) {
- keys := make([]string, 0, len(pr.data))
- for k := range pr.data {
- keys = append(keys, k)
- }
-
- return keys, nil
-}
-
-func TestFindMatchesByPackageCPE(t *testing.T) {
- matcher := match.RubyGemMatcher
+func Test_ByCPE(t *testing.T) {
tests := []struct {
- name string
- p pkg.Package
- expected []match.Match
+ name string
+ cpe cpe.CPE
+ input vulnerability.Vulnerability
+ wantErr require.ErrorAssertionFunc
+ matches bool
+ reason string
}{
{
- name: "match from range",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando1:*:ra:*:ruby:*:*"),
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"),
- },
- Name: "activerecord",
- Version: "3.7.5",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
- expected: []match.Match{
- {
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2017-fake-1",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando1:*:ra:*:ruby:*:*"),
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"),
- },
- Name: "activerecord",
- Version: "3.7.5",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"},
- Package: CPEPackageParameter{
- Name: "activerecord",
- Version: "3.7.5",
- },
- },
- Found: CPEResult{
- CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
- VersionConstraint: "< 3.7.6 (semver)",
- VulnerabilityID: "CVE-2017-fake-1",
- },
- Matcher: matcher,
- },
- },
- },
- },
- },
- {
- name: "multiple matches",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*"),
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*"),
- },
- Name: "activerecord",
- Version: "3.7.3",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
- expected: []match.Match{
- {
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2017-fake-1",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*"),
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*"),
- },
- Name: "activerecord",
- Version: "3.7.3",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
-
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*",
- },
- Namespace: "nvd:cpe",
- Package: CPEPackageParameter{
- Name: "activerecord",
- Version: "3.7.3",
- },
- },
- Found: CPEResult{
- CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
- VersionConstraint: "< 3.7.6 (semver)",
- VulnerabilityID: "CVE-2017-fake-1",
- },
- Matcher: matcher,
- },
- },
- },
- {
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2017-fake-2",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*"),
- cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*"),
- },
- Name: "activerecord",
- Version: "3.7.3",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
-
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*"},
- Namespace: "nvd:cpe",
- Package: CPEPackageParameter{
- Name: "activerecord",
- Version: "3.7.3",
- },
- },
- Found: CPEResult{
- CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"},
- VersionConstraint: "< 3.7.4 (semver)",
- VulnerabilityID: "CVE-2017-fake-2",
- },
- Matcher: matcher,
- },
- },
- },
- },
- },
- {
- name: "exact match",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*"),
- },
- Name: "activerecord",
- Version: "4.0.1",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
- expected: []match.Match{
- {
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2017-fake-3",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*"),
- },
- Name: "activerecord",
- Version: "4.0.1",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- CPEs: []string{"cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*"},
- Namespace: "nvd:cpe",
- Package: CPEPackageParameter{
- Name: "activerecord",
- Version: "4.0.1",
- },
- },
- Found: CPEResult{
- CPEs: []string{"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*"},
- VersionConstraint: "= 4.0.1 (semver)",
- VulnerabilityID: "CVE-2017-fake-3",
- },
- Matcher: matcher,
- },
- },
- },
- },
- },
- {
- name: "no match",
- p: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "couldntgetthisrightcouldyou",
- Version: "4.0.1",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
+ name: "match",
+ cpe: cpe.Must("cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*", ""),
+ input: vulnerability.Vulnerability{
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*", "")},
},
- expected: []match.Match{},
+ matches: true,
},
{
- name: "fuzzy version match",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*"),
- },
- Name: "awesome",
- Version: "98SE1",
- },
- expected: []match.Match{
- {
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2017-fake-4",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*"),
- },
- Name: "awesome",
- Version: "98SE1",
- },
-
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- CPEs: []string{"cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*"},
- Namespace: "nvd:cpe",
- Package: CPEPackageParameter{
- Name: "awesome",
- Version: "98SE1",
- },
- },
- Found: CPEResult{
- CPEs: []string{"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*"},
- VersionConstraint: "< 98SP3 (unknown)",
- VulnerabilityID: "CVE-2017-fake-4",
- },
- Matcher: matcher,
- },
- },
- },
- },
- },
- {
- name: "multiple matched CPEs",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*"),
- },
- Name: "multiple",
- Version: "1.0",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
- expected: []match.Match{
- {
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2017-fake-5",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*"),
- },
- Name: "multiple",
- Version: "1.0",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
-
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- CPEs: []string{"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*"},
- Namespace: "nvd:cpe",
- Package: CPEPackageParameter{
- Name: "multiple",
- Version: "1.0",
- },
- },
- Found: CPEResult{
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- VersionConstraint: "< 4.0 (unknown)",
- VulnerabilityID: "CVE-2017-fake-5",
- },
- Matcher: matcher,
- },
- },
- },
- },
- },
- {
- name: "filtered out match due to target_sw mismatch",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:*:*:*"),
- },
- Name: "funfun",
- Version: "5.2.1",
- Language: syftPkg.Rust,
- Type: syftPkg.RustPkg,
- },
- expected: []match.Match{},
- },
- {
- name: "target_sw mismatch with unsupported target_sw",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*"),
- },
- Name: "sw",
- Version: "0.1",
- Language: syftPkg.Go,
- Type: syftPkg.GoModulePkg,
- },
- expected: []match.Match{
- {
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2017-fake-7",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*"),
- },
- Name: "sw",
- Version: "0.1",
- Language: syftPkg.Go,
- Type: syftPkg.GoModulePkg,
- },
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- CPEs: []string{"cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*"},
- Namespace: "nvd:cpe",
- Package: CPEPackageParameter{
- Name: "sw",
- Version: "0.1",
- },
- },
- Found: CPEResult{
- CPEs: []string{
- "cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*",
- },
- VersionConstraint: "< 1.0 (unknown)",
- VulnerabilityID: "CVE-2017-fake-7",
- },
- Matcher: matcher,
- },
- },
- },
- },
- },
- {
- name: "match included even though multiple cpes are mismatch",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rust:*:*"),
- cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rails:*:*"),
- cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:ruby:*:*"),
- cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*"),
- },
- Name: "funfun",
- Version: "5.2.1",
- Language: syftPkg.Python,
- Type: syftPkg.PythonPkg,
- },
- expected: []match.Match{
- {
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2017-fake-6",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rust:*:*"),
- cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rails:*:*"),
- cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:ruby:*:*"),
- cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*"),
- },
- Name: "funfun",
- Version: "5.2.1",
- Language: syftPkg.Python,
- Type: syftPkg.PythonPkg,
- },
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- CPEs: []string{"cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*"},
- Namespace: "nvd:cpe",
- Package: CPEPackageParameter{
- Name: "funfun",
- Version: "5.2.1",
- },
- },
- Found: CPEResult{
- CPEs: []string{
- "cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*",
- "cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*",
- },
- VersionConstraint: "= 5.2.1 (python)",
- VulnerabilityID: "CVE-2017-fake-6",
- },
- Matcher: matcher,
- },
- },
- },
- },
- },
- {
- name: "Ensure target_sw mismatch does not apply to java packages",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*"),
- },
- Name: "handlebars",
- Version: "0.1",
- Language: syftPkg.Java,
- Type: syftPkg.JavaPkg,
- },
- expected: []match.Match{
- {
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2021-23369",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*"),
- },
- Name: "handlebars",
- Version: "0.1",
- Language: syftPkg.Java,
- Type: syftPkg.JavaPkg,
- },
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*"},
- Namespace: "nvd:cpe",
- Package: CPEPackageParameter{
- Name: "handlebars",
- Version: "0.1",
- },
- },
- Found: CPEResult{
- CPEs: []string{
- "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
- },
- VersionConstraint: "< 4.7.7 (unknown)",
- VulnerabilityID: "CVE-2021-23369",
- },
- Matcher: matcher,
- },
- },
- },
- },
- },
- {
- name: "Ensure target_sw mismatch does not apply to java jenkins plugins packages",
- p: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*"),
- },
- Name: "handlebars",
- Version: "0.1",
- Language: syftPkg.Java,
- Type: syftPkg.JenkinsPluginPkg,
- },
- expected: []match.Match{
- {
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2021-23369",
- },
- Package: pkg.Package{
- CPEs: []cpe.CPE{
- cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*"),
- },
- Name: "handlebars",
- Version: "0.1",
- Language: syftPkg.Java,
- Type: syftPkg.JenkinsPluginPkg,
- },
- Details: []match.Detail{
- {
- Type: match.CPEMatch,
- Confidence: 0.9,
- SearchedBy: CPEParameters{
- CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*"},
- Namespace: "nvd:cpe",
- Package: CPEPackageParameter{
- Name: "handlebars",
- Version: "0.1",
- },
- },
- Found: CPEResult{
- CPEs: []string{
- "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
- },
- VersionConstraint: "< 4.7.7 (unknown)",
- VulnerabilityID: "CVE-2021-23369",
- },
- Matcher: matcher,
- },
- },
- },
- },
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- p, err := db.NewVulnerabilityProvider(newMockStore())
- require.NoError(t, err)
- actual, err := ByPackageCPE(p, nil, test.p, matcher)
- assert.NoError(t, err)
- assertMatchesUsingIDsForVulnerabilities(t, test.expected, actual)
- for idx, e := range test.expected {
- if d := cmp.Diff(e.Details, actual[idx].Details); d != "" {
- t.Errorf("unexpected match details (-want +got):\n%s", d)
- }
- }
- })
- }
-}
-
-func TestFilterCPEsByVersion(t *testing.T) {
- tests := []struct {
- name string
- version string
- vulnerabilityCPEs []string
- expected []string
- }{
- {
- name: "filter out by simple version",
- version: "1.0",
- 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:*:*:*:*:*:*:*",
+ name: "not match",
+ cpe: cpe.Must("cpe:2.3:a:a-vendor:b-product:*:*:*:*:*:*:*:*", ""),
+ input: vulnerability.Vulnerability{
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*", "")},
},
+ matches: false,
+ reason: "CPE attributes do not match",
},
}
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- // format strings to CPE objects...
- vulnerabilityCPEs := make([]cpe.CPE, len(test.vulnerabilityCPEs))
- for idx, c := range test.vulnerabilityCPEs {
- vulnerabilityCPEs[idx] = cpe.Must(c)
- }
-
- versionObj, err := version.NewVersion(test.version, version.UnknownFormat)
- if err != nil {
- t.Fatalf("unable to get version: %+v", err)
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ constraint := ByCPE(tt.cpe)
+ matches, reason, err := constraint.MatchesVulnerability(tt.input)
+ wantErr := require.NoError
+ if tt.wantErr != nil {
+ wantErr = tt.wantErr
}
-
- // run the test subject...
- actual := filterCPEsByVersion(*versionObj, vulnerabilityCPEs)
-
- // format CPE objects to string...
- actualStrs := make([]string, len(actual))
- for idx, a := range actual {
- actualStrs[idx] = a.BindToFmtString()
- }
-
- assert.ElementsMatch(t, test.expected, actualStrs)
- })
- }
-}
-
-func TestAddMatchDetails(t *testing.T) {
- tests := []struct {
- name string
- existing []match.Detail
- new match.Detail
- expected []match.Detail
- }{
- {
- name: "append new entry -- found not equal",
- existing: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- },
- new: match.Detail{
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "totally-different-search",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "totally-different-match",
- },
- },
- },
- expected: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "totally-different-search",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "totally-different-match",
- },
- },
- },
- },
- },
- {
- name: "append new entry -- searchedBy merge fails",
- existing: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- },
- new: match.Detail{
- SearchedBy: CPEParameters{
- Namespace: "totally-different",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- expected: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- {
- SearchedBy: CPEParameters{
- Namespace: "totally-different",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- },
- },
- {
- name: "merge with exiting entry",
- existing: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- },
- new: match.Detail{
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "totally-different-search",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- expected: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- "totally-different-search",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- },
- },
- {
- name: "no addition - bad new searchedBy type",
- existing: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- },
- new: match.Detail{
- SearchedBy: "something else!",
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- expected: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- },
- },
- {
- name: "no addition - bad new found type",
- existing: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- },
- new: match.Detail{
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: "something-else!",
- },
- expected: []match.Detail{
- {
- SearchedBy: CPEParameters{
- Namespace: "nvd:cpe",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
- },
- },
- Found: CPEResult{
- VersionConstraint: "< 2.0 (unknown)",
- CPEs: []string{
- "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
- },
- },
- },
- },
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- assert.Equal(t, test.expected, addMatchDetails(test.existing, test.new))
- })
- }
-}
-
-func TestCPESearchHit_Equals(t *testing.T) {
- tests := []struct {
- name string
- current CPEResult
- other CPEResult
- expected bool
- }{
- {
- name: "different version constraint",
- current: CPEResult{
- VersionConstraint: "current-constraint",
- CPEs: []string{
- "a-cpe",
- },
- },
- other: CPEResult{
- VersionConstraint: "different-constraint",
- CPEs: []string{
- "a-cpe",
- },
- },
- expected: false,
- },
- {
- name: "different number of CPEs",
- current: CPEResult{
- VersionConstraint: "current-constraint",
- CPEs: []string{
- "a-cpe",
- },
- },
- other: CPEResult{
- VersionConstraint: "current-constraint",
- CPEs: []string{
- "a-cpe",
- "b-cpe",
- },
- },
- expected: false,
- },
- {
- name: "different CPE value",
- current: CPEResult{
- VersionConstraint: "current-constraint",
- CPEs: []string{
- "a-cpe",
- },
- },
- other: CPEResult{
- VersionConstraint: "current-constraint",
- CPEs: []string{
- "b-cpe",
- },
- },
- expected: false,
- },
- {
- name: "matches",
- current: CPEResult{
- VersionConstraint: "current-constraint",
- CPEs: []string{
- "a-cpe",
- },
- },
- other: CPEResult{
- VersionConstraint: "current-constraint",
- CPEs: []string{
- "a-cpe",
- },
- },
- expected: true,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- assert.Equal(t, test.expected, test.current.Equals(test.other))
+ wantErr(t, err)
+ assert.Equal(t, tt.matches, matches)
+ assert.Equal(t, tt.reason, reason)
})
}
}
diff --git a/grype/search/criteria.go b/grype/search/criteria.go
index c574f595570..3695b0adb85 100644
--- a/grype/search/criteria.go
+++ b/grype/search/criteria.go
@@ -1,50 +1,125 @@
package search
import (
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
+ "fmt"
+ "iter"
+ "reflect"
+ "slices"
+ "strings"
+
"github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/grype/internal/log"
)
-var (
- ByCPE Criteria = "by-cpe"
- ByLanguage Criteria = "by-language"
- ByDistro Criteria = "by-distro"
- CommonCriteria = []Criteria{
- ByLanguage,
+// ------- Utilities -------
+
+// CriteriaIterator processes all conditions into distinct sets of flattened criteria
+func CriteriaIterator(criteria []vulnerability.Criteria) iter.Seq2[int, []vulnerability.Criteria] {
+ if len(criteria) == 0 {
+ return func(_ func(int, []vulnerability.Criteria) bool) {}
}
-)
+ return func(yield func(int, []vulnerability.Criteria) bool) {
+ idx := 0
+ fn := func(criteria []vulnerability.Criteria) bool {
+ out := yield(idx, criteria)
+ idx++
+ return out
+ }
+ _ = processRemaining(nil, criteria, fn)
+ }
+}
-type Criteria string
+func processRemaining(row, criteria []vulnerability.Criteria, yield func([]vulnerability.Criteria) bool) bool {
+ if len(criteria) == 0 {
+ return yield(row)
+ }
+ return processRemainingItem(row, criteria[1:], criteria[0], yield)
+}
-func ByCriteria(store vulnerability.Provider, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType, criteria ...Criteria) ([]match.Match, error) {
- matches := make([]match.Match, 0)
- for _, c := range criteria {
- switch c {
- case ByCPE:
- m, err := ByPackageCPE(store, d, p, upstreamMatcher)
- if err != nil {
- log.Warnf("could not match by package CPE (package=%+v): %v", p, err)
- continue
+func processRemainingItem(row, criteria []vulnerability.Criteria, item vulnerability.Criteria, yield func([]vulnerability.Criteria) bool) bool {
+ switch item := item.(type) {
+ case and:
+ // we replace this criteria object with its constituent parts
+ return processRemaining(row, append(item, criteria...), yield)
+ case or:
+ for _, option := range item {
+ if !processRemainingItem(row, criteria, option, yield) {
+ return false
}
- matches = append(matches, m...)
- case ByLanguage:
- m, err := ByPackageLanguage(store, d, p, upstreamMatcher)
- if err != nil {
- log.Warnf("could not match by package language (package=%+v): %v", p, err)
+ }
+ default:
+ return processRemaining(append(row, item), criteria, yield)
+ }
+ return true // continue
+}
+
+var allowedMultipleCriteria = []reflect.Type{reflect.TypeOf(funcCriteria{})}
+
+// ValidateCriteria asserts that there are no incorrect duplications of criteria
+// e.g. multiple ByPackageName() which would result in no matches, while Or(pkgName1, pkgName2) is allowed
+func ValidateCriteria(criteria []vulnerability.Criteria) error {
+ for _, row := range CriteriaIterator(criteria) { // process OR conditions into flattened lists of AND conditions
+ seenTypes := make(map[reflect.Type]interface{})
+
+ for _, criterion := range row {
+ criterionType := reflect.TypeOf(criterion)
+
+ if slices.Contains(allowedMultipleCriteria, criterionType) {
continue
}
- matches = append(matches, m...)
- case ByDistro:
- m, err := ByPackageDistro(store, d, p, upstreamMatcher)
- if err != nil {
- log.Warnf("could not match by package distro (package=%+v): %v", p, err)
- continue
+
+ if previous, exists := seenTypes[criterionType]; exists {
+ return fmt.Errorf("multiple conflicting criteria specified: %+v %+v", previous, criterion)
}
- matches = append(matches, m...)
+
+ seenTypes[criterionType] = criterion
+ }
+ }
+ return nil
+}
+
+var _ interface {
+ vulnerability.Criteria
+} = (*or)(nil)
+
+// orCriteria provides a way to specify multiple criteria to be used, only requiring one to match
+type or []vulnerability.Criteria
+
+func Or(criteria ...vulnerability.Criteria) vulnerability.Criteria {
+ return or(criteria)
+}
+
+func (c or) MatchesVulnerability(v vulnerability.Vulnerability) (bool, string, error) {
+ var reasons []string
+ for _, crit := range c {
+ matches, reason, err := crit.MatchesVulnerability(v)
+ if matches || err != nil {
+ return matches, reason, err
+ }
+ reasons = append(reasons, reason)
+ }
+ return false, fmt.Sprintf("any(%s)", strings.Join(reasons, "; ")), nil
+}
+
+var _ interface {
+ vulnerability.Criteria
+} = (*and)(nil)
+
+// andCriteria provides a way to specify multiple criteria to be used, all required
+type and []vulnerability.Criteria
+
+func And(criteria ...vulnerability.Criteria) vulnerability.Criteria {
+ return and(criteria)
+}
+
+func (c and) MatchesVulnerability(v vulnerability.Vulnerability) (bool, string, error) {
+ var reasons []string
+
+ for _, crit := range c {
+ matches, reason, err := crit.MatchesVulnerability(v)
+ if matches || err != nil {
+ return matches, reason, err
}
+ reasons = append(reasons, reason)
}
- return matches, nil
+ return false, fmt.Sprintf("all(%s)", strings.Join(reasons, "; ")), nil
}
diff --git a/grype/search/criteria_test.go b/grype/search/criteria_test.go
new file mode 100644
index 00000000000..a3b0a25f128
--- /dev/null
+++ b/grype/search/criteria_test.go
@@ -0,0 +1,104 @@
+package search
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/distro"
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+func Test_CriteriaIterator(t *testing.T) {
+ name1 := ByPackageName("name1")
+ name2 := ByPackageName("name2")
+ name3 := ByPackageName("name3")
+
+ tests := []struct {
+ name string
+ in []vulnerability.Criteria
+ expected [][]vulnerability.Criteria
+ }{
+ {
+ name: "empty",
+ in: nil,
+ expected: nil,
+ },
+ {
+ name: "one",
+ in: []vulnerability.Criteria{name1},
+ expected: [][]vulnerability.Criteria{{name1}},
+ },
+ {
+ name: "name1 or name2",
+ in: []vulnerability.Criteria{Or(name1, name2)},
+ expected: [][]vulnerability.Criteria{{name1}, {name2}},
+ },
+ {
+ name: "name1 AND (name2 or name3)",
+ in: []vulnerability.Criteria{name1, Or(name2, name3)},
+ expected: [][]vulnerability.Criteria{{name1, name2}, {name1, name3}},
+ },
+ {
+ name: "name1 AND (name2 or name3) AND (name1 or name2 or name3)",
+ in: []vulnerability.Criteria{name1, Or(name2, name3), Or(name1, name2, name3)},
+ expected: [][]vulnerability.Criteria{
+ {name1, name2, name1}, {name1, name3, name1},
+ {name1, name2, name2}, {name1, name3, name2},
+ {name1, name2, name3}, {name1, name3, name3},
+ },
+ },
+ {
+ name: "(name1 AND name2) OR (name1 AND name3)",
+ in: []vulnerability.Criteria{Or(And(name1, name2), And(name1, name3))},
+ expected: [][]vulnerability.Criteria{
+ {name1, name2}, {name1, name3},
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var got [][]vulnerability.Criteria
+ for _, row := range CriteriaIterator(test.in) {
+ got = append(got, row)
+ }
+ require.ElementsMatch(t, test.expected, got)
+ })
+ }
+}
+
+func Test_ValidateCriteria(t *testing.T) {
+ tests := []struct {
+ name string
+ in []vulnerability.Criteria
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "no error",
+ in: []vulnerability.Criteria{ByPackageName("steve"), ByDistro(distro.Distro{})},
+ wantErr: require.NoError,
+ },
+ {
+ name: "package name error",
+ in: []vulnerability.Criteria{ByPackageName("steve"), ByPackageName("bob")},
+ wantErr: require.Error,
+ },
+ {
+ name: "multiple distros error",
+ in: []vulnerability.Criteria{ByDistro(distro.Distro{}), ByDistro(distro.Distro{})},
+ wantErr: require.Error,
+ },
+ {
+ name: "multiple package name in or condition not error",
+ in: []vulnerability.Criteria{Or(ByPackageName("steve"), ByPackageName("bob"))},
+ wantErr: require.NoError,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ err := ValidateCriteria(test.in)
+ test.wantErr(t, err)
+ })
+ }
+}
diff --git a/grype/search/distro.go b/grype/search/distro.go
index 5cd3ffe2419..65f526bf20e 100644
--- a/grype/search/distro.go
+++ b/grype/search/distro.go
@@ -2,72 +2,118 @@ package search
import (
"fmt"
+ "strings"
+ "github.com/anchore/grype/grype/db/v5/namespace"
+ distroNs "github.com/anchore/grype/grype/db/v5/namespace/distro"
"github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
)
-func ByPackageDistro(store vulnerability.ProviderByDistro, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) {
- if d == nil {
- return nil, nil
+// ByDistro returns criteria which will match vulnerabilities based on any of the provided Distros
+func ByDistro(d ...distro.Distro) vulnerability.Criteria {
+ return &DistroCriteria{
+ Distros: d,
}
+}
+
+type DistroCriteria struct {
+ Distros []distro.Distro
+}
- verObj, err := version.NewVersionFromPkg(p)
+func (c *DistroCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) {
+ ns, err := namespace.FromString(value.Namespace)
if err != nil {
- return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err)
+ 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 {
+ // not a Distro-based vulnerability
+ return false, "not a distro-based vulnerability", nil
+ }
+ if len(c.Distros) == 0 {
+ return true, "", nil
+ }
+ var distroStrs []string
+ for _, d := range c.Distros {
+ if matchesDistro(&d, dns) {
+ return true, "", nil
+ }
+ distroStrs = append(distroStrs, d.String())
}
- allPkgVulns, err := store.GetByDistro(d, p)
- if err != nil {
- return nil, fmt.Errorf("matcher failed to fetch distro=%q pkg=%q: %w", d, p.Name, err)
+ return false, fmt.Sprintf("does not match any known distro: %q", strings.Join(distroStrs, ", ")), nil
+}
+
+func (c *DistroCriteria) Summarize() string {
+ var distroStrs []string
+ for _, d := range c.Distros {
+ distroStrs = append(distroStrs, d.String())
}
+ return "does not match distro(s): " + strings.Join(distroStrs, ", ")
+}
- applicableVulns, err := onlyQualifiedPackages(d, p, allPkgVulns)
- if err != nil {
- return nil, fmt.Errorf("unable to filter distro-related vulnerabilities: %w", err)
+var _ interface {
+ vulnerability.Criteria
+} = (*DistroCriteria)(nil)
+
+// matchesDistro returns true distro types are equal and versions are compatible
+func matchesDistro(d *distro.Distro, ns *distroNs.Namespace) bool {
+ if d == nil || ns == nil {
+ return false
}
- // TODO: Port this over to a qualifier and remove
- applicableVulns, err = onlyVulnerableVersions(verObj, applicableVulns)
- if err != nil {
- return nil, fmt.Errorf("unable to filter distro-related vulnerabilities: %w", err)
+ distroType := mimicV6DistroTypeOverrides(ns.DistroType())
+ targetType := mimicV6DistroTypeOverrides(d.Type)
+ if distroType != targetType {
+ return false
+ }
+
+ return compatibleVersion(d.Version, ns.Version())
+}
+
+// compatibleVersion returns true when the versions are the same or the partial version describes the matching parts
+// of the fullVersion
+func compatibleVersion(fullVersion string, partialVersion string) bool {
+ if fullVersion == "" {
+ return true
+ }
+ if fullVersion == partialVersion {
+ return true
+ }
+ if strings.HasPrefix(fullVersion, partialVersion) && len(fullVersion) > len(partialVersion) && fullVersion[len(partialVersion)] == '.' {
+ return true
+ }
+ 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)
}
- var matches []match.Match
- for _, vuln := range applicableVulns {
- matches = append(matches, match.Match{
- Vulnerability: vuln,
- Package: p,
- Details: []match.Detail{
- {
- Type: match.ExactDirectMatch,
- Matcher: upstreamMatcher,
- SearchedBy: map[string]interface{}{
- "distro": map[string]string{
- "type": d.Type.String(),
- "version": d.RawVersion,
- },
- // 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
- // was used in the search.
- "package": map[string]string{
- "name": p.Name,
- "version": p.Version,
- },
- "namespace": vuln.Namespace,
- },
- Found: map[string]interface{}{
- "vulnerabilityID": vuln.ID,
- "versionConstraint": vuln.Constraint.String(),
- },
- Confidence: 1.0, // TODO: this is hard coded for now
- },
- },
- })
+ if replacement, exists := overrideMap[string(t)]; exists {
+ return applyMapping(replacement)
}
- return matches, err
+ return applyMapping(string(t))
}
diff --git a/grype/search/distro_test.go b/grype/search/distro_test.go
index fecc7f783a9..08bf6c6ebbc 100644
--- a/grype/search/distro_test.go
+++ b/grype/search/distro_test.go
@@ -1,167 +1,61 @@
package search
import (
- "strings"
"testing"
- "github.com/google/uuid"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
- syftPkg "github.com/anchore/syft/syft/pkg"
)
-type mockDistroProvider struct {
- data map[string]map[string][]vulnerability.Vulnerability
-}
-
-func newMockProviderByDistro() *mockDistroProvider {
- pr := mockDistroProvider{
- data: make(map[string]map[string][]vulnerability.Vulnerability),
- }
- pr.stub()
- return &pr
-}
-
-func (pr *mockDistroProvider) stub() {
- pr.data["debian:8"] = map[string][]vulnerability.Vulnerability{
- // direct...
- "neutron": {
- {
- Constraint: version.MustGetConstraint("< 2014.1.5-6", version.DebFormat),
- ID: "CVE-2014-fake-1",
- Namespace: "debian:8",
- },
- },
- }
- pr.data["sles:12.5"] = map[string][]vulnerability.Vulnerability{
- // direct...
- "sles_test_package": {
- {
- Constraint: version.MustGetConstraint("< 2014.1.5-6", version.RpmFormat),
- ID: "CVE-2014-fake-4",
- Namespace: "sles:12.5",
- },
- },
- }
-}
-
-func (pr *mockDistroProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- return pr.data[strings.ToLower(d.Type.String())+":"+d.FullVersion()][p.Name], nil
-}
-
-func TestFindMatchesByPackageDistro(t *testing.T) {
- p := pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "neutron",
- Version: "2014.1.3-6",
- Type: syftPkg.DebPkg,
- Upstreams: []pkg.UpstreamPackage{
- {
- Name: "neutron-devel",
- },
- },
- }
-
- d, err := distro.New(distro.Debian, "8", "")
- if err != nil {
- t.Fatal("could not create distro: ", err)
- }
-
- expected := []match.Match{
+func Test_ByDistro(t *testing.T) {
+ deb8, err := distro.New(distro.Debian, "8", "")
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ distro distro.Distro
+ input vulnerability.Vulnerability
+ wantErr require.ErrorAssertionFunc
+ matches bool
+ reason string
+ }{
{
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2014-fake-1",
- },
- Package: p,
- Details: []match.Detail{
- {
- Type: match.ExactDirectMatch,
- Confidence: 1,
- SearchedBy: map[string]interface{}{
- "distro": map[string]string{
- "type": "debian",
- "version": "8",
- },
- "package": map[string]string{
- "name": "neutron",
- "version": "2014.1.3-6",
- },
- "namespace": "debian:8",
- },
- Found: map[string]interface{}{
- "versionConstraint": "< 2014.1.5-6 (deb)",
- "vulnerabilityID": "CVE-2014-fake-1",
- },
- Matcher: match.PythonMatcher,
+ name: "match",
+ distro: *deb8,
+ input: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ Namespace: "debian:distro:debian:8",
},
},
+ matches: true,
},
- }
-
- store := newMockProviderByDistro()
- actual, err := ByPackageDistro(store, d, p, match.PythonMatcher)
- assert.NoError(t, err)
- assertMatchesUsingIDsForVulnerabilities(t, expected, actual)
-}
-
-func TestFindMatchesByPackageDistroSles(t *testing.T) {
- p := pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "sles_test_package",
- Version: "2014.1.3-6",
- Type: syftPkg.RpmPkg,
- Upstreams: []pkg.UpstreamPackage{
- {
- Name: "sles_test_package",
- },
- },
- }
-
- d, err := distro.New(distro.SLES, "12.5", "")
- if err != nil {
- t.Fatal("could not create distro: ", err)
- }
-
- expected := []match.Match{
{
-
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2014-fake-4",
- },
- Package: p,
- Details: []match.Detail{
- {
- Type: match.ExactDirectMatch,
- Confidence: 1,
- SearchedBy: map[string]interface{}{
- "distro": map[string]string{
- "type": "sles",
- "version": "12.5",
- },
- "package": map[string]string{
- "name": "sles_test_package",
- "version": "2014.1.3-6",
- },
- "namespace": "sles:12.5",
- },
- Found: map[string]interface{}{
- "versionConstraint": "< 2014.1.5-6 (rpm)",
- "vulnerabilityID": "CVE-2014-fake-4",
- },
- Matcher: match.PythonMatcher,
+ name: "not match",
+ distro: *deb8,
+ input: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ Namespace: "debian:distro:ubuntu:8",
},
},
+ matches: false,
+ reason: `does not match any known distro: "debian 8"`,
},
}
- store := newMockProviderByDistro()
- actual, err := ByPackageDistro(store, d, p, match.PythonMatcher)
- assert.NoError(t, err)
- assertMatchesUsingIDsForVulnerabilities(t, expected, actual)
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ constraint := ByDistro(tt.distro)
+ matches, reason, err := constraint.MatchesVulnerability(tt.input)
+ wantErr := require.NoError
+ if tt.wantErr != nil {
+ wantErr = tt.wantErr
+ }
+ wantErr(t, err)
+ assert.Equal(t, tt.matches, matches)
+ assert.Equal(t, tt.reason, reason)
+ })
+ }
}
diff --git a/grype/search/ecosystem.go b/grype/search/ecosystem.go
new file mode 100644
index 00000000000..17aa8e8df91
--- /dev/null
+++ b/grype/search/ecosystem.go
@@ -0,0 +1,49 @@
+package search
+
+import (
+ "fmt"
+
+ "github.com/anchore/grype/grype/db/v5/namespace"
+ "github.com/anchore/grype/grype/db/v5/namespace/language"
+ "github.com/anchore/grype/grype/vulnerability"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+// ByEcosystem returns criteria which will search based on the package Language and or package type
+func ByEcosystem(lang syftPkg.Language, t syftPkg.Type) vulnerability.Criteria {
+ return &EcosystemCriteria{
+ Language: lang,
+ PackageType: t,
+ }
+}
+
+type EcosystemCriteria struct {
+ Language syftPkg.Language
+ PackageType syftPkg.Type
+}
+
+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.ID, err), nil
+ }
+ lang, ok := ns.(*language.Namespace)
+ if !ok || lang == nil {
+ // not a language-based vulnerability
+ return false, "not a language-based vulnerability", nil
+ }
+
+ // TODO: add package type?
+
+ vulnLanguage := lang.Language()
+ matchesLanguage := c.Language == vulnLanguage
+ if !matchesLanguage {
+ return false, fmt.Sprintf("vulnerability language %q does not match package language %q", vulnLanguage, c.Language), nil
+ }
+
+ return true, "", nil
+}
+
+var _ interface {
+ vulnerability.Criteria
+} = (*EcosystemCriteria)(nil)
diff --git a/grype/search/ecosystem_test.go b/grype/search/ecosystem_test.go
new file mode 100644
index 00000000000..e4674f88a0d
--- /dev/null
+++ b/grype/search/ecosystem_test.go
@@ -0,0 +1,61 @@
+package search
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/vulnerability"
+ syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func Test_ByLanguage(t *testing.T) {
+ tests := []struct {
+ name string
+ lang syftPkg.Language
+ pkgType syftPkg.Type
+ input vulnerability.Vulnerability
+ wantErr require.ErrorAssertionFunc
+ matches bool
+ reason string
+ }{
+ {
+ name: "match",
+ lang: syftPkg.Java,
+ pkgType: syftPkg.JavaPkg,
+ input: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ Namespace: "github:language:java",
+ },
+ },
+ matches: true,
+ },
+ {
+ name: "not match",
+ lang: syftPkg.Java,
+ pkgType: syftPkg.JavaPkg,
+ input: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ Namespace: "github:language:javascript",
+ },
+ },
+ matches: false,
+ reason: `vulnerability language "javascript" does not match package language "java"`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ constraint := ByEcosystem(tt.lang, tt.pkgType)
+ matches, reason, err := constraint.MatchesVulnerability(tt.input)
+ wantErr := require.NoError
+ if tt.wantErr != nil {
+ wantErr = tt.wantErr
+ }
+ wantErr(t, err)
+ assert.Equal(t, tt.matches, matches)
+ assert.Equal(t, tt.reason, reason)
+ })
+ }
+}
diff --git a/grype/search/func.go b/grype/search/func.go
new file mode 100644
index 00000000000..889f2ebb09c
--- /dev/null
+++ b/grype/search/func.go
@@ -0,0 +1,19 @@
+package search
+
+import "github.com/anchore/grype/grype/vulnerability"
+
+// ByFunc returns criteria which will use the provided function to filter vulnerabilities
+func ByFunc(criteriaFunc func(vulnerability.Vulnerability) (bool, string, error)) vulnerability.Criteria {
+ return funcCriteria{fn: criteriaFunc}
+}
+
+// funcCriteria implements vulnerability.Criteria by providing a function implementing the same signature as MatchVulnerability
+type funcCriteria struct {
+ fn func(vulnerability.Vulnerability) (bool, string, error)
+}
+
+func (f funcCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) {
+ return f.fn(value)
+}
+
+var _ vulnerability.Criteria = (*funcCriteria)(nil)
diff --git a/grype/search/func_test.go b/grype/search/func_test.go
new file mode 100644
index 00000000000..26d1f53f67c
--- /dev/null
+++ b/grype/search/func_test.go
@@ -0,0 +1,53 @@
+package search
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+func Test_ByFunc(t *testing.T) {
+ tests := []struct {
+ name string
+ fn func(vulnerability.Vulnerability) (bool, string, error)
+ input vulnerability.Vulnerability
+ wantErr require.ErrorAssertionFunc
+ matches bool
+ reason string
+ }{
+ {
+ name: "match",
+ fn: func(v vulnerability.Vulnerability) (bool, string, error) {
+ return true, "", nil
+ },
+ input: vulnerability.Vulnerability{},
+ matches: true,
+ },
+ {
+ name: "not match",
+ fn: func(v vulnerability.Vulnerability) (bool, string, error) {
+ return false, "reason!", nil
+ },
+ input: vulnerability.Vulnerability{},
+ matches: false,
+ reason: "reason!",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ constraint := ByFunc(tt.fn)
+ matches, reason, err := constraint.MatchesVulnerability(tt.input)
+ wantErr := require.NoError
+ if tt.wantErr != nil {
+ wantErr = tt.wantErr
+ }
+ wantErr(t, err)
+ assert.Equal(t, tt.matches, matches)
+ assert.Equal(t, tt.reason, reason)
+ })
+ }
+}
diff --git a/grype/search/id.go b/grype/search/id.go
new file mode 100644
index 00000000000..5ebd0cd4be6
--- /dev/null
+++ b/grype/search/id.go
@@ -0,0 +1,31 @@
+package search
+
+import (
+ "fmt"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+// ByID returns criteria to search by vulnerability ID, such as CVE-2024-9143
+func ByID(id string) vulnerability.Criteria {
+ return &IDCriteria{
+ ID: id,
+ }
+}
+
+// IDCriteria is able to match vulnerabilities to the assigned ID, such as CVE-2024-1000 or GHSA-g2x7-ar59-85z5
+type IDCriteria struct {
+ ID string
+}
+
+func (v *IDCriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, string, error) {
+ matchesID := vuln.ID == v.ID
+ if !matchesID {
+ return false, fmt.Sprintf("vulnerability ID %q does not match expected ID %q", vuln.ID, v.ID), nil
+ }
+ return true, "", nil
+}
+
+var _ interface {
+ vulnerability.Criteria
+} = (*IDCriteria)(nil)
diff --git a/grype/search/id_test.go b/grype/search/id_test.go
new file mode 100644
index 00000000000..8a00451aad4
--- /dev/null
+++ b/grype/search/id_test.go
@@ -0,0 +1,57 @@
+package search
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+func Test_ByID(t *testing.T) {
+ tests := []struct {
+ name string
+ id string
+ input vulnerability.Vulnerability
+ wantErr require.ErrorAssertionFunc
+ matches bool
+ reason string
+ }{
+ {
+ name: "match",
+ id: "CVE-YEAR-1",
+ input: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-YEAR-1",
+ },
+ },
+ matches: true,
+ },
+ {
+ name: "not match",
+ id: "CVE-YEAR-1",
+ input: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-YEAR-2",
+ },
+ },
+ matches: false,
+ reason: `vulnerability ID "CVE-YEAR-2" does not match expected ID "CVE-YEAR-1"`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ constraint := ByID(tt.id)
+ matches, reason, err := constraint.MatchesVulnerability(tt.input)
+ wantErr := require.NoError
+ if tt.wantErr != nil {
+ wantErr = tt.wantErr
+ }
+ wantErr(t, err)
+ assert.Equal(t, tt.matches, matches)
+ assert.Equal(t, tt.reason, reason)
+ })
+ }
+}
diff --git a/grype/search/language.go b/grype/search/language.go
deleted file mode 100644
index 57612c45dac..00000000000
--- a/grype/search/language.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package search
-
-import (
- "fmt"
-
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/version"
- "github.com/anchore/grype/grype/vulnerability"
-)
-
-func ByPackageLanguage(store vulnerability.ProviderByLanguage, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) {
- verObj, err := version.NewVersionFromPkg(p)
- if err != nil {
- return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err)
- }
-
- allPkgVulns, err := store.GetByLanguage(p.Language, p)
- if err != nil {
- return nil, fmt.Errorf("matcher failed to fetch language=%q pkg=%q: %w", p.Language, p.Name, err)
- }
-
- applicableVulns, err := onlyQualifiedPackages(d, p, allPkgVulns)
- if err != nil {
- return nil, fmt.Errorf("unable to filter language-related vulnerabilities: %w", err)
- }
-
- // TODO: Port this over to a qualifier and remove
- applicableVulns, err = onlyVulnerableVersions(verObj, applicableVulns)
- if err != nil {
- return nil, fmt.Errorf("unable to filter language-related vulnerabilities: %w", err)
- }
-
- var matches []match.Match
- for _, vuln := range applicableVulns {
- matches = append(matches, match.Match{
-
- Vulnerability: vuln,
- Package: p,
- Details: []match.Detail{
- {
- Type: match.ExactDirectMatch,
- Confidence: 1.0, // TODO: this is hard coded for now
- Matcher: upstreamMatcher,
- SearchedBy: map[string]interface{}{
- "language": string(p.Language),
- "namespace": vuln.Namespace,
- "package": map[string]string{
- "name": p.Name,
- "version": p.Version,
- },
- },
- Found: map[string]interface{}{
- "vulnerabilityID": vuln.ID,
- "versionConstraint": vuln.Constraint.String(),
- },
- },
- },
- })
- }
-
- return matches, err
-}
diff --git a/grype/search/language_test.go b/grype/search/language_test.go
deleted file mode 100644
index 9c610a73df9..00000000000
--- a/grype/search/language_test.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package search
-
-import (
- "fmt"
- "testing"
-
- "github.com/google/uuid"
- "github.com/stretchr/testify/assert"
-
- "github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/version"
- "github.com/anchore/grype/grype/vulnerability"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-type mockLanguageProvider struct {
- data map[string]map[string][]vulnerability.Vulnerability
-}
-
-func newMockProviderByLanguage() *mockLanguageProvider {
- pr := mockLanguageProvider{
- data: make(map[string]map[string][]vulnerability.Vulnerability),
- }
- pr.stub()
- return &pr
-}
-
-func (pr *mockLanguageProvider) stub() {
- pr.data["github:gem"] = map[string][]vulnerability.Vulnerability{
- // direct...
- "activerecord": {
- {
- // make sure we find it with semVer constraint
- Constraint: version.MustGetConstraint("< 3.7.6", version.SemanticFormat),
- ID: "CVE-2017-fake-1",
- Namespace: "github:ruby",
- },
- {
- Constraint: version.MustGetConstraint("< 3.7.4", version.GemFormat),
- ID: "CVE-2017-fake-2",
- Namespace: "github:ruby",
- },
- },
- "nokogiri": {
- {
- // make sure we find it with gem version constraint
- Constraint: version.MustGetConstraint("< 1.7.6", version.GemFormat),
- ID: "CVE-2017-fake-1",
- Namespace: "github:ruby",
- },
- {
- Constraint: version.MustGetConstraint("< 1.7.4", version.SemanticFormat),
- ID: "CVE-2017-fake-2",
- Namespace: "github:ruby",
- },
- },
- }
-}
-
-func (pr *mockLanguageProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) {
- if l != syftPkg.Ruby {
- panic(fmt.Errorf("test mock only supports ruby"))
- }
- return pr.data["github:gem"][p.Name], nil
-}
-
-func expectedMatch(p pkg.Package, constraint string) []match.Match {
- return []match.Match{
- {
- Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2017-fake-1",
- },
- Package: p,
- Details: []match.Detail{
- {
- Type: match.ExactDirectMatch,
- Confidence: 1,
- SearchedBy: map[string]interface{}{
- "language": "ruby",
- "namespace": "github:ruby",
- "package": map[string]string{"name": p.Name, "version": p.Version},
- },
- Found: map[string]interface{}{
- "versionConstraint": constraint,
- "vulnerabilityID": "CVE-2017-fake-1",
- },
- Matcher: match.RubyGemMatcher,
- },
- },
- },
- }
-}
-
-func TestFindMatchesByPackageLanguage(t *testing.T) {
- cases := []struct {
- p pkg.Package
- constraint string
- }{
- {
- constraint: "< 3.7.6 (semver)",
- p: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "activerecord",
- Version: "3.7.5",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
- },
- {
- constraint: "< 1.7.6 (semver)",
- p: pkg.Package{
- ID: pkg.ID(uuid.NewString()),
- Name: "nokogiri",
- Version: "1.7.5",
- Language: syftPkg.Ruby,
- Type: syftPkg.GemPkg,
- },
- },
- }
-
- store := newMockProviderByLanguage()
- for _, c := range cases {
- t.Run(c.p.Name, func(t *testing.T) {
- actual, err := ByPackageLanguage(store, nil, c.p, match.RubyGemMatcher)
- assert.NoError(t, err)
- assertMatchesUsingIDsForVulnerabilities(t, expectedMatch(c.p, c.constraint), actual)
- })
- }
-}
diff --git a/grype/search/only_qualified_packages.go b/grype/search/only_qualified_packages.go
deleted file mode 100644
index 9fbd2c3f4f3..00000000000
--- a/grype/search/only_qualified_packages.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package search
-
-import (
- "fmt"
-
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/vulnerability"
-)
-
-func onlyQualifiedPackages(d *distro.Distro, p pkg.Package, allVulns []vulnerability.Vulnerability) ([]vulnerability.Vulnerability, error) {
- var vulns []vulnerability.Vulnerability
-
- for _, vuln := range allVulns {
- isVulnerable := true
-
- for _, q := range vuln.PackageQualifiers {
- v, err := q.Satisfied(d, p)
-
- if err != nil {
- return nil, fmt.Errorf("failed to check package qualifier=%q for distro=%q package=%q: %w", q, d, p, err)
- }
-
- isVulnerable = v
- if !isVulnerable {
- break
- }
- }
-
- if !isVulnerable {
- continue
- }
-
- vulns = append(vulns, vuln)
- }
-
- return vulns, nil
-}
diff --git a/grype/search/only_vulnerable_targets.go b/grype/search/only_vulnerable_targets.go
deleted file mode 100644
index 52dd12d6341..00000000000
--- a/grype/search/only_vulnerable_targets.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package search
-
-import (
- "github.com/facebookincubator/nvdtools/wfn"
-
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/vulnerability"
- syftPkg "github.com/anchore/syft/syft/pkg"
-)
-
-func isOSPackage(p pkg.Package) bool {
- return p.Type == syftPkg.AlpmPkg || p.Type == syftPkg.ApkPkg || p.Type == syftPkg.DebPkg || p.Type == syftPkg.KbPkg || p.Type == syftPkg.PortagePkg || p.Type == syftPkg.RpmPkg
-}
-
-func isUnknownTarget(targetSW string) bool {
- if syftPkg.LanguageByName(targetSW) != syftPkg.UnknownLanguage {
- return false
- }
-
- // There are some common target software CPE components which are not currently
- // supported by syft but are signifcant sources of false positives and should be
- // considered known for the purposes of filtering here
- known := map[string]bool{
- "wordpress": true,
- "wordpress_": true,
- "joomla": true,
- "joomla\\!": true,
- "drupal": true,
- }
-
- if _, ok := known[targetSW]; ok {
- return false
- }
-
- return true
-}
-
-// Determines if a vulnerability is an accurate match using the vulnerability's cpes' target software
-func onlyVulnerableTargets(p pkg.Package, allVulns []vulnerability.Vulnerability) []vulnerability.Vulnerability {
- var vulns []vulnerability.Vulnerability
-
- // Exclude OS package types from this logic, since they could be embedding any type of ecosystem package
- if isOSPackage(p) {
- return allVulns
- }
-
- // There are quite a few cases within java where other ecosystem components (particularly javascript packages)
- // are embedded directly within jar files, so we can't yet make this assumption with java as it will cause dropping
- // of valid vulnerabilities that syft has specific logic https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go#L48-L75
- // to ensure will be surfaced
- if p.Language == syftPkg.Java {
- return allVulns
- }
-
- for _, vuln := range allVulns {
- isPackageVulnerable := len(vuln.CPEs) == 0
- for _, cpe := range vuln.CPEs {
- targetSW := cpe.TargetSW
- mismatchWithUnknownLanguage := targetSW != string(p.Language) && isUnknownTarget(targetSW)
- if targetSW == wfn.Any || targetSW == wfn.NA || targetSW == string(p.Language) || mismatchWithUnknownLanguage {
- isPackageVulnerable = true
- }
- }
-
- if !isPackageVulnerable {
- continue
- }
-
- vulns = append(vulns, vuln)
- }
-
- return vulns
-}
diff --git a/grype/search/only_vulnerable_targets_test.go b/grype/search/only_vulnerable_targets_test.go
deleted file mode 100644
index 02e5ec301bf..00000000000
--- a/grype/search/only_vulnerable_targets_test.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package search
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func Test_isUnknownTarget(t *testing.T) {
- tests := []struct {
- name string
- targetSW string
- expected bool
- }{
- {name: "supported syft language", targetSW: "python", expected: false},
- {name: "supported non-syft language CPE component", targetSW: "wordpress", expected: false},
- {name: "unknown component", targetSW: "abc", expected: true},
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- u := isUnknownTarget(test.targetSW)
- assert.Equal(t, test.expected, u)
- })
- }
-}
diff --git a/grype/search/only_vulnerable_versions.go b/grype/search/only_vulnerable_versions.go
deleted file mode 100644
index 482429d4f2a..00000000000
--- a/grype/search/only_vulnerable_versions.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package search
-
-import (
- "errors"
- "fmt"
-
- "github.com/anchore/grype/grype/version"
- "github.com/anchore/grype/grype/vulnerability"
- "github.com/anchore/grype/internal/log"
-)
-
-func onlyVulnerableVersions(verObj *version.Version, allVulns []vulnerability.Vulnerability) ([]vulnerability.Vulnerability, error) {
- var vulns []vulnerability.Vulnerability
-
- for _, vuln := range allVulns {
- isPackageVulnerable, err := vuln.Constraint.Satisfied(verObj)
- if err != nil {
- var e *version.NonFatalConstraintError
- if errors.As(err, &e) {
- log.Warn(e)
- } else {
- return nil, fmt.Errorf("failed to check constraint=%q version=%q: %w", vuln.Constraint, verObj, err)
- }
- }
-
- if !isPackageVulnerable {
- continue
- }
-
- vulns = append(vulns, vuln)
- }
-
- return vulns, nil
-}
diff --git a/grype/search/package_name.go b/grype/search/package_name.go
new file mode 100644
index 00000000000..0ecad9a1851
--- /dev/null
+++ b/grype/search/package_name.go
@@ -0,0 +1,31 @@
+package search
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+// ByPackageName returns criteria restricting vulnerabilities to match the package name provided
+func ByPackageName(packageName string) vulnerability.Criteria {
+ return &PackageNameCriteria{
+ PackageName: packageName,
+ }
+}
+
+type PackageNameCriteria struct {
+ PackageName string
+}
+
+func (v *PackageNameCriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, string, error) {
+ matchesPackageName := strings.EqualFold(vuln.PackageName, v.PackageName)
+ if !matchesPackageName {
+ return false, fmt.Sprintf("vulnerability package name %q does not match expected package name %q", vuln.PackageName, v.PackageName), nil
+ }
+ return true, "", nil
+}
+
+var _ interface {
+ vulnerability.Criteria
+} = (*PackageNameCriteria)(nil)
diff --git a/grype/search/package_name_test.go b/grype/search/package_name_test.go
new file mode 100644
index 00000000000..990a487e8a9
--- /dev/null
+++ b/grype/search/package_name_test.go
@@ -0,0 +1,61 @@
+package search
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+func Test_ByPackageName(t *testing.T) {
+ tests := []struct {
+ name string
+ packageName string
+ input vulnerability.Vulnerability
+ wantErr require.ErrorAssertionFunc
+ matches bool
+ reason string
+ }{
+ {
+ name: "match",
+ packageName: "some-name",
+ input: vulnerability.Vulnerability{
+ PackageName: "some-name",
+ },
+ matches: true,
+ },
+ {
+ name: "match case insensitive",
+ packageName: "some-name",
+ input: vulnerability.Vulnerability{
+ PackageName: "SomE-NaMe",
+ },
+ matches: true,
+ },
+ {
+ name: "not match",
+ packageName: "some-name",
+ input: vulnerability.Vulnerability{
+ PackageName: "other-name",
+ },
+ matches: false,
+ reason: `vulnerability package name "other-name" does not match expected package name "some-name"`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ constraint := ByPackageName(tt.packageName)
+ matches, reason, err := constraint.MatchesVulnerability(tt.input)
+ wantErr := require.NoError
+ if tt.wantErr != nil {
+ wantErr = tt.wantErr
+ }
+ wantErr(t, err)
+ assert.Equal(t, tt.matches, matches)
+ assert.Equal(t, tt.reason, reason)
+ })
+ }
+}
diff --git a/grype/search/version_constraint.go b/grype/search/version_constraint.go
new file mode 100644
index 00000000000..aed3f153649
--- /dev/null
+++ b/grype/search/version_constraint.go
@@ -0,0 +1,93 @@
+package search
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/log"
+)
+
+// VersionConstraintMatcher is used for searches which include version.Constraints; this should be used instead of
+// post-filtering vulnerabilities in order to most efficiently hydrate data in memory
+type VersionConstraintMatcher interface {
+ MatchesConstraint(constraint version.Constraint) (bool, error)
+}
+
+// ByConstraintFunc returns criteria which will use the provided function as inclusion criteria
+func ByConstraintFunc(constraintFunc func(constraint version.Constraint) (bool, error)) vulnerability.Criteria {
+ return &constraintFuncCriteria{fn: constraintFunc}
+}
+
+// ByVersion returns criteria which constrains vulnerabilities to those with matching version constraints
+func ByVersion(v version.Version) vulnerability.Criteria {
+ return ByConstraintFunc(func(constraint version.Constraint) (bool, error) {
+ satisfied, err := constraint.Satisfied(&v)
+ if err != nil {
+ var formatErr *version.UnsupportedFormatError
+ if errors.As(err, &formatErr) {
+ // if the format is unsupported, then the constraint is not satisfied, but this should not be conveyed as an error
+ log.WithFields("reason", err).Trace("unsatisfied constraint")
+ return false, nil
+ }
+
+ var e *version.NonFatalConstraintError
+ if errors.As(err, &e) {
+ log.Warn(e)
+ } else {
+ return false, fmt.Errorf("failed to check constraint=%v version=%v: %w", constraint, v, err)
+ }
+ }
+ return satisfied, nil
+ })
+}
+
+// constraintFuncCriteria implements vulnerability.Criteria by providing a function implementing the same signature as MatchVulnerability
+type constraintFuncCriteria struct {
+ fn func(constraint version.Constraint) (bool, error)
+ summary string
+}
+
+func (f *constraintFuncCriteria) MatchesConstraint(constraint version.Constraint) (bool, error) {
+ return f.fn(constraint)
+}
+
+func (f *constraintFuncCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) {
+ matches, err := f.fn(value.Constraint)
+ // TODO: should we do something about this?
+ return matches, "", err
+}
+
+func (f *constraintFuncCriteria) Summarize() string {
+ return f.summary
+}
+
+var _ interface {
+ vulnerability.Criteria
+ VersionConstraintMatcher
+} = (*constraintFuncCriteria)(nil)
+
+func MultiConstraintMatcher(a, b VersionConstraintMatcher) VersionConstraintMatcher {
+ return &multiConstraintMatcher{
+ a: a,
+ b: b,
+ }
+}
+
+// multiConstraintMatcher is used internally when multiple version constraint matchers are specified
+type multiConstraintMatcher struct {
+ a, b VersionConstraintMatcher
+}
+
+func (m *multiConstraintMatcher) MatchesConstraint(constraint version.Constraint) (bool, error) {
+ a, err := m.a.MatchesConstraint(constraint)
+ if a || err != nil {
+ return a, err
+ }
+ return m.b.MatchesConstraint(constraint)
+}
+
+var _ interface {
+ VersionConstraintMatcher
+} = (*multiConstraintMatcher)(nil)
diff --git a/grype/search/version_constraint_test.go b/grype/search/version_constraint_test.go
new file mode 100644
index 00000000000..2fbfbbe1381
--- /dev/null
+++ b/grype/search/version_constraint_test.go
@@ -0,0 +1,103 @@
+package search
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+func Test_ByVersion(t *testing.T) {
+ tests := []struct {
+ name string
+ version string
+ input vulnerability.Vulnerability
+ wantErr require.ErrorAssertionFunc
+ matches bool
+ reason string
+ }{
+ {
+ name: "match",
+ version: "1.0",
+ input: vulnerability.Vulnerability{
+ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat),
+ },
+ matches: true,
+ },
+ {
+ name: "not match",
+ version: "2.0",
+ input: vulnerability.Vulnerability{
+ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat),
+ },
+ matches: false,
+ reason: "", // we don't expect a reason to be raised up at this level
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ v, err := version.NewVersion(tt.version, version.SemanticFormat)
+ require.NoError(t, err)
+ constraint := ByVersion(*v)
+ matches, reason, err := constraint.MatchesVulnerability(tt.input)
+ wantErr := require.NoError
+ if tt.wantErr != nil {
+ wantErr = tt.wantErr
+ }
+ wantErr(t, err)
+ assert.Equal(t, tt.matches, matches)
+ assert.Equal(t, tt.reason, reason)
+ })
+ }
+}
+
+func Test_ByConstraintFunc(t *testing.T) {
+ tests := []struct {
+ name string
+ constraintFunc func(version.Constraint) (bool, error)
+ input vulnerability.Vulnerability
+ wantErr require.ErrorAssertionFunc
+ matches bool
+ reason string
+ }{
+ {
+ name: "match",
+ constraintFunc: func(version.Constraint) (bool, error) {
+ return true, nil
+ },
+ input: vulnerability.Vulnerability{
+ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat),
+ },
+ matches: true,
+ },
+ {
+ name: "not match",
+ constraintFunc: func(version.Constraint) (bool, error) {
+ return false, nil
+ },
+ input: vulnerability.Vulnerability{
+ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat),
+ },
+ matches: false,
+ reason: "", // we don't expect a reason to be raised up at this level
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ constraint := ByConstraintFunc(tt.constraintFunc)
+ matches, reason, err := constraint.MatchesVulnerability(tt.input)
+ wantErr := require.NoError
+ if tt.wantErr != nil {
+ wantErr = tt.wantErr
+ }
+ wantErr(t, err)
+ assert.Equal(t, tt.matches, matches)
+ assert.Equal(t, tt.reason, reason)
+ })
+ }
+}
diff --git a/grype/version/apk_constraint.go b/grype/version/apk_constraint.go
index 9a70ae84260..679a405819d 100644
--- a/grype/version/apk_constraint.go
+++ b/grype/version/apk_constraint.go
@@ -10,7 +10,7 @@ type apkConstraint struct {
func newApkConstraint(raw string) (apkConstraint, error) {
if raw == "" {
- // empy constraints are always satisfied
+ // empty constraints are always satisfied
return apkConstraint{}, nil
}
@@ -54,7 +54,7 @@ func (c apkConstraint) Satisfied(version *Version) (bool, error) {
}
if !c.supported(version.Format) {
- return false, fmt.Errorf("(apk) unsupported format: %s", version.Format)
+ return false, NewUnsupportedFormatError(ApkFormat, version.Format)
}
if version.rich.apkVer == nil {
diff --git a/grype/version/apk_version.go b/grype/version/apk_version.go
index 6ca7253e325..a70c2941560 100644
--- a/grype/version/apk_version.go
+++ b/grype/version/apk_version.go
@@ -22,9 +22,11 @@ func newApkVersion(raw string) (*apkVersion, error) {
}
func (a *apkVersion) Compare(other *Version) (int, error) {
- if other.Format != ApkFormat {
- return -1, fmt.Errorf("unable to compare apk to given format: %s", other.Format)
+ other, err := finalizeComparisonVersion(other, ApkFormat)
+ if err != nil {
+ return -1, err
}
+
if other.rich.apkVer == nil {
return -1, fmt.Errorf("given empty apkVersion object")
}
diff --git a/grype/version/apk_version_test.go b/grype/version/apk_version_test.go
new file mode 100644
index 00000000000..43584f09f53
--- /dev/null
+++ b/grype/version/apk_version_test.go
@@ -0,0 +1,130 @@
+package version
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestApkVersionCompare(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "same format successful comparison",
+ thisVersion: "1.2.3-r4",
+ otherVersion: "1.2.3-r5",
+ otherFormat: ApkFormat,
+ expectError: false,
+ },
+ {
+ name: "different format returns error",
+ thisVersion: "1.2.3-r4",
+ otherVersion: "1.2.3",
+ otherFormat: SemanticFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - deb",
+ thisVersion: "1.2.3-r4",
+ otherVersion: "1.2.3-1",
+ otherFormat: DebFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "unknown format attempts upgrade - valid apk format",
+ thisVersion: "1.2.3-r4",
+ otherVersion: "1.2.3-r5",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ {
+ name: "unknown format attempts upgrade - invalid apk format",
+ thisVersion: "1.2.3-r4",
+ otherVersion: "not-valid-apk-format",
+ otherFormat: UnknownFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, err := newApkVersion(test.thisVersion)
+ require.NoError(t, err)
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestApkVersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (*apkVersion, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (*apkVersion, *Version) {
+ thisVer, _ := newApkVersion("1.2.3-r4")
+ return thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "empty apkVersion in other object",
+ setupFunc: func() (*apkVersion, *Version) {
+ thisVer, _ := newApkVersion("1.2.3-r4")
+ otherVer := &Version{
+ Raw: "1.2.3-r5",
+ Format: ApkFormat,
+ rich: rich{}, // don't populate the rich.apkVer field
+ }
+
+ return thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty apkVersion object",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/comparator.go b/grype/version/comparator.go
index dbc6a0752d7..e77cab12306 100644
--- a/grype/version/comparator.go
+++ b/grype/version/comparator.go
@@ -5,3 +5,22 @@ type comparatorGenerator func(constraintUnit) (Comparator, error)
type Comparator interface {
Compare(*Version) (int, error)
}
+
+func finalizeComparisonVersion(version *Version, targetFormat Format) (*Version, error) {
+ if version == nil {
+ return nil, ErrNoVersionProvided
+ }
+ switch version.Format {
+ case targetFormat:
+ return version, nil
+ case UnknownFormat:
+ upgradedVersion, err := NewVersion(version.Raw, targetFormat)
+ if err != nil {
+ // unable to upgrade the unknown version to the target version
+ return nil, NewUnsupportedFormatError(targetFormat, version.Format)
+ }
+ return upgradedVersion, nil
+ }
+
+ return nil, NewUnsupportedFormatError(targetFormat, version.Format)
+}
diff --git a/grype/version/constraint.go b/grype/version/constraint.go
index 5f9b5b7ab18..00a83ce56e0 100644
--- a/grype/version/constraint.go
+++ b/grype/version/constraint.go
@@ -17,19 +17,20 @@ func GetConstraint(constStr string, format Format) (Constraint, error) {
return newSemanticConstraint(constStr)
case DebFormat:
return newDebConstraint(constStr)
+ case GolangFormat:
+ return newGolangConstraint(constStr)
+ case MavenFormat:
+ return newMavenConstraint(constStr)
case RpmFormat:
return newRpmConstraint(constStr)
case PythonFormat:
- // This is specific to PythonFormat so that it adheres to PEP440 and its odd corner-cases
- // It is significantly odd enough, that the fuzzyConstraint is the best bet to compare versions.
- // Although this will work in most cases, some oddities aren't supported, like:
- // 1.0b2.post345.dev456 which is allowed by the spec. In that case (a dev release of a post release)
- // the comparator will fail. See https://www.python.org/dev/peps/pep-0440
- return newFuzzyConstraint(constStr, "python")
+ return newPep440Constraint(constStr)
case KBFormat:
return newKBConstraint(constStr)
case PortageFormat:
return newPortageConstraint(constStr)
+ case JVMFormat:
+ return newJvmConstraint(constStr)
case UnknownFormat:
return newFuzzyConstraint(constStr, "unknown")
}
diff --git a/grype/version/constraint_expression.go b/grype/version/constraint_expression.go
index 601e5d4f6f7..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) {
@@ -58,7 +69,7 @@ func (c *constraintExpression) satisfied(other *Version) (bool, error) {
for j, andUnit := range andOperand {
result, err := andUnit.Compare(other)
if err != nil {
- return false, fmt.Errorf("uncomparable %+v %+v: %w", andUnit, other, err)
+ return false, fmt.Errorf("uncomparable %#v vs %q: %w", andUnit, other.String(), err)
}
unit := c.units[i][j]
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/deb_constraint.go b/grype/version/deb_constraint.go
index 5950bf170e6..d70125f1efb 100644
--- a/grype/version/deb_constraint.go
+++ b/grype/version/deb_constraint.go
@@ -49,7 +49,7 @@ func (c debConstraint) Satisfied(version *Version) (bool, error) {
}
if !c.supported(version.Format) {
- return false, fmt.Errorf("(deb) unsupported format: %s", version.Format)
+ return false, NewUnsupportedFormatError(DebFormat, version.Format)
}
if version.rich.debVer == nil {
diff --git a/grype/version/deb_version.go b/grype/version/deb_version.go
index 947561a24f5..08d412668b8 100644
--- a/grype/version/deb_version.go
+++ b/grype/version/deb_version.go
@@ -21,9 +21,11 @@ func newDebVersion(raw string) (*debVersion, error) {
}
func (d *debVersion) Compare(other *Version) (int, error) {
- if other.Format != DebFormat {
- return -1, fmt.Errorf("unable to compare deb to given format: %s", other.Format)
+ other, err := finalizeComparisonVersion(other, DebFormat)
+ if err != nil {
+ return -1, err
}
+
if other.rich.debVer == nil {
return -1, fmt.Errorf("given empty debVersion object")
}
diff --git a/grype/version/deb_version_test.go b/grype/version/deb_version_test.go
new file mode 100644
index 00000000000..d3cceb42c9c
--- /dev/null
+++ b/grype/version/deb_version_test.go
@@ -0,0 +1,131 @@
+package version
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDebVersionCompare(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "same format successful comparison",
+ thisVersion: "1.2.3-1",
+ otherVersion: "1.2.3-2",
+ otherFormat: DebFormat,
+ expectError: false,
+ },
+ {
+ name: "different format returns error",
+ thisVersion: "1.2.3-1",
+ otherVersion: "1.2.3",
+ otherFormat: SemanticFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - apk",
+ thisVersion: "1.2.3-1",
+ otherVersion: "1.2.3-r4",
+ otherFormat: ApkFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "unknown format attempts upgrade - valid deb format",
+ thisVersion: "1.2.3-1",
+ otherVersion: "1.2.3-2",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ {
+ name: "unknown format attempts upgrade - invalid deb format",
+ thisVersion: "1.2.3-1",
+ otherVersion: "not-valid-deb-format",
+ otherFormat: UnknownFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, err := newDebVersion(test.thisVersion)
+ require.NoError(t, err)
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestDebVersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (*debVersion, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (*debVersion, *Version) {
+ thisVer, _ := newDebVersion("1.2.3-1")
+ return thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "empty debVersion in other object",
+ setupFunc: func() (*debVersion, *Version) {
+ thisVer, _ := newDebVersion("1.2.3-1")
+
+ otherVer := &Version{
+ Raw: "1.2.3-2",
+ Format: DebFormat,
+ rich: rich{}, // debVer will be nil
+ }
+
+ return thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty debVersion object",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/error.go b/grype/version/error.go
new file mode 100644
index 00000000000..d0244035cd0
--- /dev/null
+++ b/grype/version/error.go
@@ -0,0 +1,36 @@
+package version
+
+import (
+ "errors"
+ "fmt"
+)
+
+var ErrNoVersionProvided = errors.New("no version provided for comparison")
+
+// UnsupportedFormatError represents an error when a format doesn't match the expected format
+type UnsupportedFormatError struct {
+ Left Format
+ Right Format
+}
+
+// NewUnsupportedFormatError creates a new UnsupportedFormatError
+func NewUnsupportedFormatError(left, right Format) *UnsupportedFormatError {
+ return &UnsupportedFormatError{
+ Left: left,
+ Right: right,
+ }
+}
+
+func (e *UnsupportedFormatError) Error() string {
+ return fmt.Sprintf("(%s) unsupported version format for comparison: %s", e.Left, e.Right)
+}
+
+func (e *UnsupportedFormatError) Is(target error) bool {
+ var t *UnsupportedFormatError
+ ok := errors.As(target, &t)
+ if !ok {
+ return false
+ }
+ return (t.Left == UnknownFormat || t.Left == e.Left) &&
+ (t.Right == UnknownFormat || t.Right == e.Right)
+}
diff --git a/grype/version/format.go b/grype/version/format.go
index fb9821f76fa..c4a47c89570 100644
--- a/grype/version/format.go
+++ b/grype/version/format.go
@@ -3,7 +3,8 @@ package version
import (
"strings"
- "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/grype/grype/pkg"
+ syftPkg "github.com/anchore/syft/syft/pkg"
)
const (
@@ -11,36 +12,45 @@ const (
SemanticFormat
ApkFormat
DebFormat
+ MavenFormat
RpmFormat
PythonFormat
KBFormat
GemFormat
PortageFormat
+ GolangFormat
+ JVMFormat
)
type Format int
var formatStr = []string{
- "UnknownFormat",
+ "Unknown",
"Semantic",
"Apk",
"Deb",
+ "Maven",
"RPM",
"Python",
"KB",
"Gem",
"Portage",
+ "Go",
+ "JVM",
}
var Formats = []Format{
SemanticFormat,
ApkFormat,
DebFormat,
+ MavenFormat,
RpmFormat,
PythonFormat,
KBFormat,
GemFormat,
PortageFormat,
+ GolangFormat,
+ JVMFormat,
}
func ParseFormat(userStr string) Format {
@@ -51,6 +61,10 @@ func ParseFormat(userStr string) Format {
return ApkFormat
case strings.ToLower(DebFormat.String()), "dpkg":
return DebFormat
+ case strings.ToLower(GolangFormat.String()), "go":
+ return GolangFormat
+ case strings.ToLower(MavenFormat.String()), "maven":
+ return MavenFormat
case strings.ToLower(RpmFormat.String()), "rpm":
return RpmFormat
case strings.ToLower(PythonFormat.String()), "python":
@@ -61,31 +75,39 @@ func ParseFormat(userStr string) Format {
return GemFormat
case strings.ToLower(PortageFormat.String()), "portage":
return PortageFormat
+ case strings.ToLower(JVMFormat.String()), "jvm", "jre", "jdk", "openjdk", "jep223":
+ return JVMFormat
}
return UnknownFormat
}
-func FormatFromPkgType(t pkg.Type) Format {
- var format Format
- switch t {
- case pkg.ApkPkg:
- format = ApkFormat
- case pkg.DebPkg:
- format = DebFormat
- case pkg.RpmPkg:
- format = RpmFormat
- case pkg.GemPkg:
- format = GemFormat
- case pkg.PythonPkg:
- format = PythonFormat
- case pkg.KbPkg:
- format = KBFormat
- case pkg.PortagePkg:
- format = PortageFormat
- default:
- format = UnknownFormat
+func FormatFromPkg(p pkg.Package) Format {
+ switch p.Type {
+ case syftPkg.ApkPkg:
+ return ApkFormat
+ case syftPkg.DebPkg:
+ return DebFormat
+ case syftPkg.JavaPkg:
+ return MavenFormat
+ case syftPkg.RpmPkg:
+ return RpmFormat
+ case syftPkg.GemPkg:
+ return GemFormat
+ case syftPkg.PythonPkg:
+ return PythonFormat
+ case syftPkg.KbPkg:
+ return KBFormat
+ case syftPkg.PortagePkg:
+ return PortageFormat
+ case syftPkg.GoModulePkg:
+ return GolangFormat
+ }
+
+ if pkg.IsJvmPackage(p) {
+ return JVMFormat
}
- return format
+
+ return UnknownFormat
}
func (f Format) String() string {
diff --git a/grype/version/format_test.go b/grype/version/format_test.go
index a05e6027e46..5471ebaa159 100644
--- a/grype/version/format_test.go
+++ b/grype/version/format_test.go
@@ -4,7 +4,8 @@ import (
"fmt"
"testing"
- "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/grype/grype/pkg"
+ syftPkg "github.com/anchore/syft/syft/pkg"
)
func TestParseFormat(t *testing.T) {
@@ -16,6 +17,10 @@ func TestParseFormat(t *testing.T) {
input: "dpkg",
format: DebFormat,
},
+ {
+ input: "maven",
+ format: MavenFormat,
+ },
{
input: "gem",
format: GemFormat,
@@ -47,25 +52,78 @@ func TestParseFormat(t *testing.T) {
func TestFormatFromPkgType(t *testing.T) {
tests := []struct {
- pkgType pkg.Type
- format Format
+ name string
+ p pkg.Package
+ format Format
}{
{
- pkgType: pkg.DebPkg,
- format: DebFormat,
+ name: "deb",
+ p: pkg.Package{
+ Type: syftPkg.DebPkg,
+ },
+ format: DebFormat,
+ },
+ {
+ name: "java jar",
+ p: pkg.Package{
+ Type: syftPkg.JavaPkg,
+ },
+ format: MavenFormat,
+ },
+ {
+ name: "gem",
+ p: pkg.Package{
+ Type: syftPkg.GemPkg,
+ },
+ format: GemFormat,
+ },
+ {
+ name: "jvm by metadata",
+ p: pkg.Package{
+ Metadata: pkg.JavaVMInstallationMetadata{},
+ },
+ format: JVMFormat,
+ },
+ {
+ name: "jvm by type and name (jdk)",
+ p: pkg.Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "jdk",
+ },
+ format: JVMFormat,
+ },
+ {
+ name: "jvm by type and name (openjdk)",
+ p: pkg.Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "openjdk",
+ },
+ format: JVMFormat,
+ },
+ {
+ name: "jvm by type and name (jre)",
+ p: pkg.Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "jre",
+ },
+ format: JVMFormat,
},
{
- pkgType: pkg.GemPkg,
- format: GemFormat,
+ name: "jvm by type and name (java_se)",
+ p: pkg.Package{
+ Type: syftPkg.BinaryPkg,
+ Name: "java_se",
+ },
+ format: JVMFormat,
},
}
for _, test := range tests {
- name := fmt.Sprintf("pkgType[%s]->format[%s]", test.pkgType, test.format)
+ name := fmt.Sprintf("pkgType[%s]->format[%s]", test.p.Type, test.format)
t.Run(name, func(t *testing.T) {
- actual := FormatFromPkgType(test.pkgType)
+ actual := FormatFromPkg(test.p)
if actual != test.format {
- t.Errorf("mismatched pkgType->format mapping, pkgType='%s': '%s'!='%s'", test.pkgType, test.format, actual)
+ t.Errorf("mismatched pkgType->format mapping, pkgType='%s': '%s'!='%s'", test.p.Type, test.format, actual)
}
})
}
diff --git a/grype/version/fuzzy_constraint.go b/grype/version/fuzzy_constraint.go
index abda758d501..8cf0ac32b77 100644
--- a/grype/version/fuzzy_constraint.go
+++ b/grype/version/fuzzy_constraint.go
@@ -9,7 +9,7 @@ import (
)
// derived from https://semver.org/, but additionally matches partial versions (e.g. "2.0")
-var pseudoSemverPattern = regexp.MustCompile(`^(0|[1-9]\d*)(\.(0|[1-9]\d*))?(\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
+var pseudoSemverPattern = regexp.MustCompile(`^(0|[1-9]\d*)(\.(0|[1-9]\d*))?(\.(0|[1-9]\d*))?(?:(-|alpha|beta|rc)((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
type fuzzyConstraint struct {
rawPhrase string
@@ -38,7 +38,7 @@ func newFuzzyConstraint(phrase, hint string) (*fuzzyConstraint, error) {
check:
for _, units := range constraints.units {
for _, unit := range units {
- if !pseudoSemverPattern.Match([]byte(unit.version)) {
+ if !pseudoSemverPattern.MatchString(unit.version) {
valid = false
break check
}
@@ -79,9 +79,22 @@ func (f *fuzzyConstraint) Satisfied(verObj *Version) (bool, error) {
version := verObj.Raw
+ // rebuild temp constraint based off of ver obj
+ if verObj.Format != UnknownFormat {
+ newConstaint, err := GetConstraint(f.rawPhrase, verObj.Format)
+ // check if constraint is not fuzzyConstraint
+ _, ok := newConstaint.(*fuzzyConstraint)
+ if err == nil && !ok {
+ satisfied, err := newConstaint.Satisfied(verObj)
+ if err == nil {
+ return satisfied, nil
+ }
+ }
+ }
+
// attempt semver first, then fallback to fuzzy part matching...
if f.semanticConstraint != nil {
- if pseudoSemverPattern.Match([]byte(version)) {
+ if pseudoSemverPattern.MatchString(version) {
if semver, err := newSemanticVersion(version); err == nil && semver != nil {
return f.semanticConstraint.Check(semver.verObj), nil
}
@@ -163,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/fuzzy_constraint_test.go b/grype/version/fuzzy_constraint_test.go
index 150a383e30e..650ad50bafe 100644
--- a/grype/version/fuzzy_constraint_test.go
+++ b/grype/version/fuzzy_constraint_test.go
@@ -47,6 +47,18 @@ func TestSmartVerCmp(t *testing.T) {
{"10.0", "1.000.0.1", 1},
{"1.0.4", "1.0.4+metadata", -1}, // this is also somewhat wrong, however, there is a semver parser that can handle this case (which should be leveraged when possible)
{"1.3.2-r0", "1.3.3-r0", -1}, // regression: regression for https://github.com/anchore/go-version/pull/2
+ // Java JRE/JDK versioning prior to the implementing https://openjdk.org/jeps/223 for >= version 9
+ {"1.8.0_456", "1.8.0", 1},
+ {"1.8.0_456", "1.8.0_234", 1},
+ {"1.8.0_456", "1.8.0_457", -1},
+ {"1.8.0_456-b1", "1.8.0_456-b2", -1},
+ {"1.8.0_456", "1.8.0_456-b1", -1},
+ // Also check the semver equivalents of pre java version 9 work as expected:
+ {"8.0.456", "8.0", 1},
+ {"8.0.456", "8.0.234", 1},
+ {"8.0.456", "8.0.457", -1},
+ {"8.0.456+1", "8.0.456+2", -1},
+ {"8.0.456", "8.0.456+1", -1},
}
for _, c := range cases {
t.Run(fmt.Sprintf("%q vs %q", c.v1, c.v2), func(t *testing.T) {
@@ -264,6 +276,102 @@ func TestFuzzyConstraintSatisfaction(t *testing.T) {
constraint: "> v1.5",
satisfied: true,
},
+ {
+ name: "rc candidates with no '-' can match semver pattern",
+ version: "1.20rc1",
+ constraint: " = 1.20.0-rc1",
+ satisfied: true,
+ },
+ {
+ name: "candidates ahead of alpha",
+ version: "3.11.0",
+ constraint: "> 3.11.0-alpha1",
+ satisfied: true,
+ },
+ {
+ name: "candidates ahead of beta",
+ version: "3.11.0",
+ constraint: "> 3.11.0-beta1",
+ satisfied: true,
+ },
+ {
+ name: "candidates ahead of same alpha versions",
+ version: "3.11.0-alpha5",
+ constraint: "> 3.11.0-alpha1",
+ satisfied: true,
+ },
+ {
+ name: "candidates are placed correctly between alpha and release",
+ version: "3.11.0-beta5",
+ constraint: "3.11.0 || = 3.11.0-alpha1",
+ satisfied: false,
+ },
+ {
+ name: "candidates with letter suffix are alphabetically greater than their versions",
+ version: "1.0.2a",
+ constraint: " < 1.0.2w",
+ satisfied: true,
+ },
+ {
+ name: "candidates with multiple letter suffix are alphabetically greater than their versions",
+ version: "1.0.2zg",
+ constraint: " < 1.0.2zh",
+ satisfied: true,
+ },
+ {
+ name: "candidates with pre suffix are sorted numerically",
+ version: "1.0.2pre1",
+ constraint: " < 1.0.2pre2",
+ satisfied: true,
+ },
+ {
+ name: "candidates with letter suffix and r0 are alphabetically greater than their versions",
+ version: "1.0.2k-r0",
+ constraint: " < 1.0.2l-r0",
+ satisfied: true,
+ },
+ {
+ name: "openssl version with letter suffix and r0 are alphabetically greater than their versions",
+ version: "1.0.2k-r0",
+ constraint: ">= 1.0.2",
+ satisfied: true,
+ },
+ {
+ name: "openssl versions with letter suffix and r0 are alphabetically greater than their versions and compared equally to other lettered versions",
+ version: "1.0.2k-r0",
+ constraint: ">= 1.0.2, < 1.0.2m",
+ satisfied: true,
+ },
+ {
+ name: "openssl pre2 is still considered less than release",
+ version: "1.1.1-pre2",
+ constraint: "> 1.1.1-pre1, < 1.1.1",
+ satisfied: true,
+ },
+ {
+ name: "major version releases are less than their subsequent patch releases with letter suffixes",
+ version: "1.1.1",
+ constraint: "> 1.1.1-a",
+ satisfied: true,
+ },
+ {
+ name: "go pseudoversion vulnerable: version is less, want less",
+ version: "0.0.0-20230716120725-531d2d74bc12",
+ constraint: "<0.0.0-20230922105210-14b16010c2ee",
+ satisfied: true,
+ },
+ {
+ name: "go pseudoversion not vulnerable: same version but constraint is less",
+ version: "0.0.0-20230922105210-14b16010c2ee",
+ constraint: "<0.0.0-20230922105210-14b16010c2ee",
+ satisfied: false,
+ },
+ {
+ name: "go pseudoversion not vulnerable: greater version",
+ version: "0.0.0-20230922112808-5421fefb8386",
+ constraint: "<0.0.0-20230922105210-14b16010c2ee",
+ satisfied: false,
+ },
}
for _, test := range tests {
@@ -275,3 +383,20 @@ func TestFuzzyConstraintSatisfaction(t *testing.T) {
})
}
}
+
+func TestPseudoSemverPattern(t *testing.T) {
+ tests := []struct {
+ name string
+ version string
+ valid bool
+ }{
+ {name: "rc candidates are valid semver", version: "1.2.3-rc1", valid: true},
+ {name: "rc candidates with no dash are valid semver", version: "1.2.3rc1", valid: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.valid, pseudoSemverPattern.MatchString(test.version))
+ })
+ }
+}
diff --git a/grype/version/fuzzy_version.go b/grype/version/fuzzy_version.go
index 37d2d717546..9f31438ac46 100644
--- a/grype/version/fuzzy_version.go
+++ b/grype/version/fuzzy_version.go
@@ -25,6 +25,9 @@ func newFuzzyVersion(raw string) (fuzzyVersion, error) {
}
func (v *fuzzyVersion) Compare(other *Version) (int, error) {
+ if other == nil {
+ return -1, ErrNoVersionProvided
+ }
// check if both versions can be compared as semvers...
if other.Format == SemanticFormat && v.semVer != nil {
if other.rich.semVer == nil {
diff --git a/grype/version/fuzzy_version_test.go b/grype/version/fuzzy_version_test.go
new file mode 100644
index 00000000000..e79241a6fb0
--- /dev/null
+++ b/grype/version/fuzzy_version_test.go
@@ -0,0 +1,144 @@
+package version
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestFuzzyVersionCompare(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "fuzzy comparison with semantic version",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: SemanticFormat,
+ expectError: false,
+ },
+ {
+ name: "fuzzy comparison with unknown format",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ {
+ name: "fuzzy comparison with different format",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3-r4",
+ otherFormat: ApkFormat,
+ expectError: false,
+ },
+ {
+ name: "fuzzy comparison with non-semantic string",
+ thisVersion: "1.2.3",
+ otherVersion: "abc123",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ {
+ name: "fuzzy comparison with empty strings",
+ thisVersion: "1.2.3",
+ otherVersion: "",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer := fuzzyVersion{
+ raw: test.thisVersion,
+ }
+
+ // if thisVersion is semantic-compatible, populate the semVer field
+ if semver, err := newSemanticVersion(test.thisVersion); err == nil {
+ thisVer.semVer = semver
+ }
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestFuzzyVersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (*fuzzyVersion, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (*fuzzyVersion, *Version) {
+ thisVer := &fuzzyVersion{
+ raw: "1.2.3",
+ }
+ if semver, err := newSemanticVersion("1.2.3"); err == nil {
+ thisVer.semVer = semver
+ }
+ return thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "semantic format but empty semver object",
+ setupFunc: func() (*fuzzyVersion, *Version) {
+ thisVer := &fuzzyVersion{
+ raw: "1.2.3",
+ }
+ if semver, err := newSemanticVersion("1.2.3"); err == nil {
+ thisVer.semVer = semver
+ }
+
+ otherVer := &Version{
+ Raw: "1.2.4",
+ Format: SemanticFormat,
+ rich: rich{}, // semVer will be nil
+ }
+
+ return thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty semver object (fuzzy)",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/generic_constraint.go b/grype/version/generic_constraint.go
new file mode 100644
index 00000000000..ebd039e290d
--- /dev/null
+++ b/grype/version/generic_constraint.go
@@ -0,0 +1,38 @@
+package version
+
+import "fmt"
+
+var _ Constraint = (*genericConstraint)(nil)
+
+type genericConstraint struct {
+ raw string
+ expression constraintExpression
+ name string
+}
+
+func newGenericConstraint(raw string, genFn comparatorGenerator, name string) (genericConstraint, error) {
+ constraints, err := newConstraintExpression(raw, genFn)
+ if err != nil {
+ return genericConstraint{}, err
+ }
+ return genericConstraint{
+ expression: constraints,
+ raw: raw,
+ name: name,
+ }, nil
+}
+
+func (g genericConstraint) String() string {
+ value := "none"
+ if g.raw != "" {
+ value = g.raw
+ }
+ return fmt.Sprintf("%s (%s)", value, g.name)
+}
+
+func (g genericConstraint) Satisfied(version *Version) (bool, error) {
+ if g.raw == "" {
+ return true, nil // the empty constraint is always satisfied
+ }
+ return g.expression.satisfied(version)
+}
diff --git a/grype/version/golang_constraint.go b/grype/version/golang_constraint.go
new file mode 100644
index 00000000000..f9443311a3d
--- /dev/null
+++ b/grype/version/golang_constraint.go
@@ -0,0 +1,15 @@
+package version
+
+import "fmt"
+
+func newGolangConstraint(raw string) (Constraint, error) {
+ return newGenericConstraint(raw, newGolangComparator, "go")
+}
+
+func newGolangComparator(unit constraintUnit) (Comparator, error) {
+ ver, err := newGolangVersion(unit.version)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse Golang constraint version (%s): %w", unit.version, err)
+ }
+ return ver, nil
+}
diff --git a/grype/version/golang_constraint_test.go b/grype/version/golang_constraint_test.go
new file mode 100644
index 00000000000..4a73767af01
--- /dev/null
+++ b/grype/version/golang_constraint_test.go
@@ -0,0 +1,81 @@
+package version
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGolangConstraints(t *testing.T) {
+ tests := []struct {
+ name string
+ version string
+ constraint string
+ satisfied bool
+ }{
+ {
+ name: "regular semantic version satisfied",
+ version: "v1.2.3",
+ constraint: "< 1.2.4",
+ satisfied: true,
+ },
+ {
+ name: "regular semantic version unsatisfied",
+ version: "v1.2.3",
+ constraint: "> 1.2.4",
+ satisfied: false,
+ },
+ {
+ name: "+incompatible added to version", // see grype#1581
+ version: "v3.2.0+incompatible",
+ constraint: "<=3.2.0",
+ satisfied: true,
+ },
+ {
+ name: "the empty constraint is always satisfied",
+ version: "v1.0.0",
+ constraint: "",
+ satisfied: true,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ c, err := newGolangConstraint(tc.constraint)
+ require.NoError(t, err)
+ v, err := NewVersion(tc.version, GolangFormat)
+ require.NoError(t, err)
+ sat, err := c.Satisfied(v)
+ require.NoError(t, err)
+ assert.Equal(t, tc.satisfied, sat)
+ })
+ }
+}
+
+func TestString(t *testing.T) {
+ tests := []struct {
+ name string
+ constraint string
+ expected string
+ }{
+ {
+ name: "empty string",
+ constraint: "",
+ expected: "none (go)",
+ },
+ {
+ name: "basic constraint",
+ constraint: "< 1.3.4",
+ expected: "< 1.3.4 (go)",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ c, err := newGolangConstraint(tc.constraint)
+ require.NoError(t, err)
+ assert.Equal(t, tc.expected, c.String())
+ })
+ }
+}
diff --git a/grype/version/golang_version.go b/grype/version/golang_version.go
new file mode 100644
index 00000000000..0be1aa92b25
--- /dev/null
+++ b/grype/version/golang_version.go
@@ -0,0 +1,75 @@
+package version
+
+import (
+ "fmt"
+ "strings"
+
+ hashiVer "github.com/anchore/go-version"
+)
+
+var _ Comparator = (*golangVersion)(nil)
+
+type golangVersion struct {
+ raw string
+ semVer *hashiVer.Version
+}
+
+func newGolangVersion(v string) (*golangVersion, error) {
+ if v == "(devel)" {
+ return nil, ErrUnsupportedVersion
+ }
+
+ // Invalid Semver fix ups
+
+ // go stdlib is reported by syft as a go package with version like "go1.24.1"
+ // other versions have "v" as a prefix, which the semver lib handles automatically
+ fixedUp := strings.TrimPrefix(v, "go")
+
+ // go1.24 creates non-dot separated build metadata fields, e.g. +incompatible+dirty
+ // Fix up as per semver spec
+ before, after, found := strings.Cut(fixedUp, "+")
+ if found {
+ fixedUp = before + "+" + strings.ReplaceAll(after, "+", ".")
+ }
+
+ semver, err := hashiVer.NewSemver(fixedUp)
+ if err != nil {
+ return nil, err
+ }
+ return &golangVersion{
+ raw: v,
+ semVer: semver,
+ }, nil
+}
+
+func (g golangVersion) Compare(other *Version) (int, error) {
+ other, err := finalizeComparisonVersion(other, GolangFormat)
+ if err != nil {
+ return -1, err
+ }
+
+ if other.rich.golangVersion == nil {
+ return -1, fmt.Errorf("cannot compare version with nil golang version to golang version")
+ }
+ if other.rich.golangVersion.raw == g.raw {
+ return 0, nil
+ }
+ if other.rich.golangVersion.raw == "(devel)" {
+ return -1, fmt.Errorf("cannot compare %s with %s", g.raw, other.rich.golangVersion.raw)
+ }
+
+ return other.rich.golangVersion.compare(g), nil
+}
+
+func (g golangVersion) compare(o golangVersion) int {
+ switch {
+ case g.semVer != nil && o.semVer != nil:
+ return g.semVer.Compare(o.semVer)
+ case g.semVer != nil && o.semVer == nil:
+ return 1
+ case g.semVer == nil && o.semVer != nil:
+ return -1
+ default:
+ return strings.Compare(g.raw, o.raw)
+ }
+}
diff --git a/grype/version/golang_version_test.go b/grype/version/golang_version_test.go
new file mode 100644
index 00000000000..deab9ae711c
--- /dev/null
+++ b/grype/version/golang_version_test.go
@@ -0,0 +1,212 @@
+package version
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ hashiVer "github.com/anchore/go-version"
+)
+
+func TestNewGolangVersion(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected golangVersion
+ wantErr bool
+ }{
+ {
+ name: "normal semantic version",
+ input: "v1.8.0",
+ expected: golangVersion{
+ raw: "v1.8.0",
+ semVer: hashiVer.Must(hashiVer.NewSemver("v1.8.0")),
+ },
+ },
+ {
+ name: "v0.0.0 date and hash version",
+ input: "v0.0.0-20180116102854-5a71ef0e047d",
+ expected: golangVersion{
+ raw: "v0.0.0-20180116102854-5a71ef0e047d",
+ semVer: hashiVer.Must(hashiVer.NewSemver("v0.0.0-20180116102854-5a71ef0e047d")),
+ },
+ },
+ {
+ name: "semver with +incompatible",
+ input: "v24.0.7+incompatible",
+ expected: golangVersion{
+ raw: "v24.0.7+incompatible",
+ semVer: hashiVer.Must(hashiVer.NewSemver("v24.0.7+incompatible")),
+ },
+ },
+ {
+ name: "semver with +incompatible+dirty",
+ input: "v24.0.7+incompatible+dirty",
+ expected: golangVersion{
+ raw: "v24.0.7+incompatible+dirty",
+ semVer: hashiVer.Must(hashiVer.NewSemver("v24.0.7+incompatible.dirty")),
+ },
+ },
+ {
+ name: "standard library",
+ input: "go1.21.4",
+ expected: golangVersion{
+ raw: "go1.21.4",
+ semVer: hashiVer.Must(hashiVer.NewSemver("1.21.4")),
+ },
+ },
+ {
+ // "(devel)" is the main module of a go program.
+ // If we get a package with this version, it means the SBOM
+ // doesn't have a real version number for the built package, so
+ // we can't compare it and should just return an error.
+ name: "devel",
+ input: "(devel)",
+ wantErr: true,
+ },
+ {
+ name: "invalid input",
+ input: "some nonsense",
+ wantErr: true,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ v, err := newGolangVersion(tc.input)
+ if tc.wantErr {
+ require.Error(t, err)
+ return
+ }
+ assert.Nil(t, err)
+ assert.Equal(t, tc.expected, *v)
+ })
+ }
+}
+
+func TestCompareGolangVersions(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ want int
+ }{
+ {
+ name: "semver this version less",
+ thisVersion: "v1.2.3",
+ otherVersion: "v1.2.4",
+ want: -1,
+ },
+ {
+ name: "semver this version more",
+ thisVersion: "v1.3.4",
+ otherVersion: "v1.2.4",
+ want: 1,
+ },
+ {
+ name: "semver equal",
+ thisVersion: "v1.2.4",
+ otherVersion: "v1.2.4",
+ want: 0,
+ },
+ {
+ name: "commit-sha this version less",
+ thisVersion: "v0.0.0-20180116102854-5a71ef0e047d",
+ otherVersion: "v0.0.0-20190116102854-somehash",
+ want: -1,
+ },
+ {
+ name: "commit-sha this version more",
+ thisVersion: "v0.0.0-20180216102854-5a71ef0e047d",
+ otherVersion: "v0.0.0-20180116102854-somehash",
+ want: 1,
+ },
+ {
+ name: "commit-sha this version equal",
+ thisVersion: "v0.0.0-20180116102854-5a71ef0e047d",
+ otherVersion: "v0.0.0-20180116102854-5a71ef0e047d",
+ want: 0,
+ },
+ {
+ name: "this pre-semver is less than any semver",
+ thisVersion: "v0.0.0-20180116102854-5a71ef0e047d",
+ otherVersion: "v0.0.1",
+ want: -1,
+ },
+ {
+ name: "semver is greater than timestamp",
+ thisVersion: "v2.1.0",
+ otherVersion: "v0.0.0-20180116102854-5a71ef0e047d",
+ want: 1,
+ },
+ {
+ name: "pseudoversion less than other pseudoversion",
+ thisVersion: "v0.0.0-20170116102854-1ef0e047d5a7",
+ otherVersion: "v0.0.0-20180116102854-5a71ef0e047d",
+ want: -1,
+ },
+ {
+ name: "pseudoversion greater than other pseudoversion",
+ thisVersion: "v0.0.0-20190116102854-8a3f0e047d5a",
+ otherVersion: "v0.0.0-20180116102854-5a71ef0e047d",
+ want: 1,
+ },
+ {
+ name: "+incompatible doesn't break equality",
+ thisVersion: "v3.2.0",
+ otherVersion: "v3.2.0+incompatible",
+ want: 0,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ a, err := newGolangVersion(tc.thisVersion)
+ require.NoError(t, err)
+ other, err := newGolangVersion(tc.otherVersion)
+ require.NoError(t, err)
+ got := a.compare(*other)
+ assert.Equal(t, tc.want, got)
+ })
+ }
+}
+
+func Test_newGolangVersion_UnsupportedVersion(t *testing.T) {
+ tests := []struct {
+ name string
+ v string
+ want *golangVersion
+ wantErr assert.ErrorAssertionFunc
+ }{
+ {
+ name: "devel",
+ v: "(devel)",
+ wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool {
+ return assert.ErrorIs(t, err, ErrUnsupportedVersion)
+ },
+ },
+ {
+ name: "invalid",
+ v: "invalid",
+ wantErr: assert.Error,
+ },
+ {
+ name: "valid",
+ v: "v1.2.3",
+ want: &golangVersion{
+ raw: "v1.2.3",
+ semVer: hashiVer.Must(hashiVer.NewSemver("v1.2.3")),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := newGolangVersion(tt.v)
+ if tt.wantErr != nil {
+ tt.wantErr(t, err)
+ }
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/grype/version/jvm_constraint.go b/grype/version/jvm_constraint.go
new file mode 100644
index 00000000000..1f7930f4d5e
--- /dev/null
+++ b/grype/version/jvm_constraint.go
@@ -0,0 +1,15 @@
+package version
+
+import "fmt"
+
+func newJvmConstraint(raw string) (Constraint, error) {
+ return newGenericConstraint(raw, newJvmComparator, "jvm")
+}
+
+func newJvmComparator(unit constraintUnit) (Comparator, error) {
+ ver, err := newJvmVersion(unit.version)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse JVM constraint version (%s): %w", unit.version, err)
+ }
+ return ver, nil
+}
diff --git a/grype/version/jvm_constraint_test.go b/grype/version/jvm_constraint_test.go
new file mode 100644
index 00000000000..b587662826c
--- /dev/null
+++ b/grype/version/jvm_constraint_test.go
@@ -0,0 +1,70 @@
+package version
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestVersionConstraintJVM(t *testing.T) {
+ tests := []testCase{
+ // pre jep 223 versions
+ {version: "1.7.0_80", constraint: "< 1.8.0", satisfied: true},
+ {version: "1.8.0_131", constraint: "> 1.8.0", satisfied: true},
+ {version: "1.8.0_131", constraint: "< 1.8.0_132", satisfied: true},
+ {version: "1.8.0_131-b11", constraint: "< 1.8.0_132", satisfied: true},
+
+ {version: "1.7.0_80", constraint: "> 1.8.0", satisfied: false},
+ {version: "1.8.0_131", constraint: "< 1.8.0", satisfied: false},
+ {version: "1.8.0_131", constraint: "> 1.8.0_132", satisfied: false},
+ {version: "1.8.0_131-b11", constraint: "> 1.8.0_132", satisfied: false},
+
+ {version: "1.7.0_80", constraint: "= 1.8.0", satisfied: false},
+ {version: "1.8.0_131", constraint: "= 1.8.0", satisfied: false},
+ {version: "1.8.0_131", constraint: "= 1.8.0_132", satisfied: false},
+ {version: "1.8.0_131-b11", constraint: "= 1.8.0_132", satisfied: false},
+
+ {version: "1.8.0_80", constraint: "= 1.8.0_80", satisfied: true},
+ {version: "1.8.0_131", constraint: ">= 1.8.0_131", satisfied: true},
+ {version: "1.8.0_131", constraint: "= 1.8.0_131-b001", satisfied: true}, // builds should not matter
+ {version: "1.8.0_131-ea-b11", constraint: "= 1.8.0_131-ea", satisfied: true},
+
+ // jep 223 versions
+ {version: "8.0.4", constraint: "> 8.0.3", satisfied: true},
+ {version: "8.0.4", constraint: "< 8.0.5", satisfied: true},
+ {version: "9.0.0", constraint: "> 8.0.5", satisfied: true},
+ {version: "9.0.0", constraint: "< 9.1.0", satisfied: true},
+ {version: "11.0.4", constraint: "<= 11.0.4", satisfied: true},
+ {version: "11.0.5", constraint: "> 11.0.4", satisfied: true},
+
+ {version: "8.0.4", constraint: "< 8.0.3", satisfied: false},
+ {version: "8.0.4", constraint: "> 8.0.5", satisfied: false},
+ {version: "9.0.0", constraint: "< 8.0.5", satisfied: false},
+ {version: "9.0.0", constraint: "> 9.1.0", satisfied: false},
+ {version: "11.0.4", constraint: "> 11.0.4", satisfied: false},
+ {version: "11.0.5", constraint: "< 11.0.4", satisfied: false},
+
+ // mixed versions
+ {version: "1.8.0_131", constraint: "< 9.0.0", satisfied: true}, // 1.8.0_131 -> 8.0.131
+ {version: "9.0.0", constraint: "> 1.8.0_131", satisfied: true}, // 1.8.0_131 -> 8.0.131
+ {version: "1.8.0_131", constraint: "<= 8.0.131", satisfied: true},
+ {version: "1.8.0_131", constraint: "> 7.0.79", satisfied: true},
+ {version: "1.8.0_131", constraint: "= 8.0.131", satisfied: true},
+ {version: "1.8.0_131", constraint: ">= 9.0.0", satisfied: false},
+ {version: "9.0.1", constraint: "< 8.0.131", satisfied: false},
+
+ // pre-release versions
+ {version: "1.8.0_131-ea", constraint: "< 1.8.0_131", satisfied: true},
+ {version: "1.8.0_131", constraint: "> 1.8.0_131-ea", satisfied: true},
+ {version: "9.0.0-ea", constraint: "< 9.0.0", satisfied: true},
+ {version: "9.0.0-ea", constraint: "> 1.8.0_131", satisfied: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.version+"_constraint_"+test.constraint, func(t *testing.T) {
+ constraint, err := newJvmConstraint(test.constraint)
+ require.NoError(t, err)
+ test.assertVersionConstraint(t, JVMFormat, constraint)
+ })
+ }
+}
diff --git a/grype/version/jvm_version.go b/grype/version/jvm_version.go
new file mode 100644
index 00000000000..2d43cb35ffc
--- /dev/null
+++ b/grype/version/jvm_version.go
@@ -0,0 +1,171 @@
+package version
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ hashiVer "github.com/anchore/go-version"
+ "github.com/anchore/grype/internal"
+ "github.com/anchore/grype/internal/log"
+)
+
+var _ Comparator = (*jvmVersion)(nil)
+
+var (
+ preJep223VersionPattern = regexp.MustCompile(`^1\.(?P\d+)(\.(?P\d+)([_-](update)?(_)?(?P\d+))?(-(?P[^b][^-]+))?(-b(?P\d+))?)?`)
+ nonCompliantSemverIsh = regexp.MustCompile(`^(?P\d+)(\.(?P\d+)(\.(?P\d+))?([_-](update)?(_)?(?P\d+))?(-(?P[^b][^-]+))?(-b(?P\d+))?)?`)
+)
+
+type jvmVersion struct {
+ isPreJep223 bool
+ semVer *hashiVer.Version
+}
+
+func newJvmVersion(raw string) (*jvmVersion, error) {
+ isPreJep233 := strings.HasPrefix(raw, "1.")
+
+ if isPreJep233 {
+ // convert the pre-JEP 223 version to semver
+ raw = convertPreJep223Version(raw)
+ } else {
+ raw = convertNonCompliantSemver(raw)
+ }
+ verObj, err := hashiVer.NewVersion(raw)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create semver obj for JVM version: %w", err)
+ }
+
+ return &jvmVersion{
+ isPreJep223: isPreJep233,
+ semVer: verObj,
+ }, nil
+}
+
+func (v *jvmVersion) Compare(other *Version) (int, error) {
+ if other == nil {
+ return -1, ErrNoVersionProvided
+ }
+
+ if other.Format == JVMFormat {
+ if other.rich.jvmVersion == nil {
+ return -1, fmt.Errorf("given empty jvmVersion object")
+ }
+ return other.rich.jvmVersion.compare(*v), nil
+ }
+
+ if other.Format == SemanticFormat {
+ if other.rich.semVer == nil {
+ return -1, fmt.Errorf("given empty semVer object")
+ }
+ return other.rich.semVer.verObj.Compare(v.semVer), nil
+ }
+
+ jvmUpgrade, err := finalizeComparisonVersion(other, JVMFormat)
+ if err == nil {
+ if jvmUpgrade.rich.jvmVersion == nil {
+ return -1, fmt.Errorf("given empty jvmVersion object")
+ }
+ return jvmUpgrade.rich.jvmVersion.compare(*v), nil
+ }
+
+ semUpgrade, err := finalizeComparisonVersion(other, SemanticFormat)
+ if err == nil {
+ if semUpgrade.rich.semVer == nil {
+ return -1, fmt.Errorf("given empty semVer object")
+ }
+ return semUpgrade.rich.semVer.verObj.Compare(v.semVer), nil
+ }
+
+ return -1, NewUnsupportedFormatError(JVMFormat, other.Format)
+}
+
+func (v jvmVersion) compare(other jvmVersion) int {
+ return v.semVer.Compare(other.semVer)
+}
+
+func convertNonCompliantSemver(version string) string {
+ // if there is -update as a prerelease, and the patch version is missing or 0, then we should parse the prerelease
+ // info that has the update value and extract the version. This should be used as the patch version.
+
+ // 8.0-update302 --> 8.0.302
+ // 8.0-update302-b08 --> 8.0.302+8
+ // 8.0-update_302-b08 --> 8.0.302+8
+
+ matches := internal.MatchNamedCaptureGroups(nonCompliantSemverIsh, version)
+ if len(matches) == 0 {
+ log.WithFields("version", version).Trace("unable to convert pre-JEP 223 JVM version")
+ return version
+ }
+
+ // extract relevant parts from the matches
+ majorVersion := trim0sFromLeft(matches["major"])
+ minorVersion := trim0sFromLeft(matches["minor"])
+ patchVersion := trim0sFromLeft(matches["patch"])
+ update := trim0sFromLeft(matches["update"])
+ preRelease := trim0sFromLeft(matches["prerelease"])
+ build := trim0sFromLeft(matches["build"])
+
+ if (patchVersion == "" || patchVersion == "0") && update != "" {
+ patchVersion = update
+ }
+
+ return buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build)
+}
+
+func convertPreJep223Version(version string) string {
+ // convert the following pre JEP 223 version strings to semvers
+ // 1.8.0_302-b08 --> 8.0.302+8
+ // 1.9.0-ea-b19 --> 9.0.0-ea+19
+ // NOTE: this makes an assumption that the old update field is the patch version in semver...
+ // this is NOT strictly in the spec, but for 1.8 this tends to be true (especially for temurin-based builds)
+ version = strings.TrimSpace(version)
+
+ matches := internal.MatchNamedCaptureGroups(preJep223VersionPattern, version)
+ if len(matches) == 0 {
+ log.WithFields("version", version).Trace("unable to convert pre-JEP 223 JVM version")
+ return version
+ }
+
+ // extract relevant parts from the matches
+ majorVersion := trim0sFromLeft(matches["major"])
+ minorVersion := trim0sFromLeft(matches["minor"])
+ patchVersion := trim0sFromLeft(matches["patch"])
+ preRelease := trim0sFromLeft(matches["prerelease"])
+ build := trim0sFromLeft(matches["build"])
+
+ if patchVersion == "" {
+ patchVersion = "0"
+ }
+
+ return buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build)
+}
+func buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build string) string {
+ if minorVersion == "" {
+ minorVersion = "0"
+ }
+
+ segs := []string{majorVersion, minorVersion}
+ if patchVersion != "" {
+ segs = append(segs, patchVersion)
+ }
+
+ var semver strings.Builder
+ semver.WriteString(strings.Join(segs, "."))
+
+ if preRelease != "" {
+ semver.WriteString(fmt.Sprintf("-%s", preRelease))
+ }
+ if build != "" {
+ semver.WriteString(fmt.Sprintf("+%s", build))
+ }
+
+ return semver.String()
+}
+
+func trim0sFromLeft(v string) string {
+ if v == "0" {
+ return v
+ }
+ return strings.TrimLeft(v, "0")
+}
diff --git a/grype/version/jvm_version_test.go b/grype/version/jvm_version_test.go
new file mode 100644
index 00000000000..a1e0df8e952
--- /dev/null
+++ b/grype/version/jvm_version_test.go
@@ -0,0 +1,305 @@
+package version
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestVersionJVM(t *testing.T) {
+ tests := []struct {
+ v1 string
+ v2 string
+ expected int
+ }{
+ // pre jep223 versions
+ {"1.8", "1.8.0", 0},
+ {"1.8.0", "1.8.0_0", 0},
+ {"1.8.0", "1.8.0", 0},
+ {"1.7.0", "1.8.0", -1},
+ {"1.8.0_131", "1.8.0_131", 0},
+ {"1.8.0_131", "1.8.0_132", -1},
+
+ // builds should not matter
+ {"1.8.0_131", "1.8.0_130", 1},
+ {"1.8.0_131", "1.8.0_132-b11", -1},
+ {"1.8.0_131-b11", "1.8.0_132-b11", -1},
+ {"1.8.0_131-b11", "1.8.0_131-b12", 0},
+ {"1.8.0_131-b11", "1.8.0_131-b10", 0},
+ {"1.8.0_131-b11", "1.8.0_131", 0},
+ {"1.8.0_131-b11", "1.8.0_131-b11", 0},
+
+ // jep223 versions (semver)
+ {"8.0.4", "8.0.4", 0},
+ {"8.0.4", "8.0.5", -1},
+ {"8.0.4", "8.0.3", 1},
+ {"8.0.4", "8.0.4+b1", 0},
+
+ // mix comparison
+ {"1.8.0_131", "8.0.4", 1}, // 1.8.0_131 --> 8.0.131
+ {"8.0.4", "1.8.0_131", -1}, // doesn't matter which side the comparison is on
+ {"1.8.0_131-b002", "8.0.131+b2", 0}, // builds should not matter
+ {"1.8.0_131-b002", "8.0.131+b1", 0}, // builds should not matter
+ {"1.6.0", "8.0.1", -1}, // 1.6.0 --> 6.0.0
+
+ // prerelease
+ {"1.8.0_13-ea-b002", "1.8.0_13-ea-b001", 0},
+ {"1.8.0_13-ea", "1.8.0_13-ea-b001", 0},
+ {"1.8.0_13-ea-b002", "8.0.13-ea+b2", 0},
+ {"1.8.0_13-ea-b002", "8.0.13+b2", -1},
+ {"1.8.0_13-b002", "8.0.13-ea+b2", 1},
+
+ // pre 1.8 (when the jep 223 was introduced)
+ {"1.7.0", "7.0.0", 0}, // there is no v7 of the JVM, but we want to honor this comparison since it may be someone mistakenly using the wrong version format
+
+ // invalid but we should work with these
+ {"1.8.0_131", "1.8.0-update131-b02", 0},
+ {"1.8.0_131", "1.8.0-update_131-b02", 0},
+ }
+
+ for _, test := range tests {
+ name := test.v1 + "_vs_" + test.v2
+ t.Run(name, func(t *testing.T) {
+ v1, err := newJvmVersion(test.v1)
+ require.NotNil(t, v1)
+ require.NoError(t, err)
+
+ v2, err := newJvmVersion(test.v2)
+ require.NotNil(t, v2)
+ require.NoError(t, err)
+
+ actual := v1.compare(*v2)
+ assert.Equal(t, test.expected, actual)
+ })
+ }
+}
+
+func TestConvertNonCompliantSemver(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "simple update",
+ input: "8.0-update302",
+ expected: "8.0.302",
+ },
+ {
+ name: "update with build",
+ input: "8.0-update302-b08",
+ expected: "8.0.302+8",
+ },
+ {
+ name: "update with underscore and build",
+ input: "8.0-update_302-b08",
+ expected: "8.0.302+8",
+ },
+ {
+ name: "version without patch and prerelease",
+ input: "8.0.0",
+ expected: "8.0.0",
+ },
+ {
+ name: "version with patch, no update",
+ input: "8.0.100",
+ expected: "8.0.100",
+ },
+ {
+ name: "version with patch and prerelease",
+ input: "8.0.0-rc1",
+ expected: "8.0.0-rc1",
+ },
+ {
+ name: "invalid update format, no update keyword",
+ input: "8.0-foo302",
+ expected: "8.0-foo302",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := convertNonCompliantSemver(tt.input)
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+func TestVersionJVM_invalid(t *testing.T) {
+ tests := []struct {
+ name string
+ version string
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "invalid version",
+ version: "1.a",
+ wantErr: require.Error,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.wantErr == nil {
+ tt.wantErr = require.NoError
+ }
+ v, err := newJvmVersion(tt.version)
+ assert.Nil(t, v)
+ tt.wantErr(t, err)
+ })
+ }
+}
+
+func TestJvmVersionCompare_Formats(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "same format successful comparison",
+ thisVersion: "1.8.0_275",
+ otherVersion: "1.8.0_281",
+ otherFormat: JVMFormat,
+ expectError: false,
+ },
+ {
+ name: "semantic format successful comparison",
+ thisVersion: "1.8.0_275",
+ otherVersion: "1.8.1",
+ otherFormat: SemanticFormat,
+ expectError: false,
+ },
+ {
+ name: "unknown format attempts upgrade to JVM - valid",
+ thisVersion: "1.8.0_275",
+ otherVersion: "1.8.0_281",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ {
+ name: "unknown format attempts upgrade to Semantic - valid",
+ thisVersion: "1.8.0_275",
+ otherVersion: "1.9.0",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ {
+ name: "unknown format fails all upgrades - invalid",
+ thisVersion: "1.8.0_275",
+ otherVersion: "not-valid-jvm-or-semver",
+ otherFormat: UnknownFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - apk",
+ thisVersion: "1.8.0_275",
+ otherVersion: "1.8.0-r1",
+ otherFormat: ApkFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - deb",
+ thisVersion: "1.8.0_275",
+ otherVersion: "1.8.0-1",
+ otherFormat: DebFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, err := newJvmVersion(test.thisVersion)
+ require.NoError(t, err)
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestJvmVersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (*jvmVersion, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (*jvmVersion, *Version) {
+ thisVer, _ := newJvmVersion("1.8.0_275")
+ return thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "jvm format but empty jvmVersion object",
+ setupFunc: func() (*jvmVersion, *Version) {
+ thisVer, _ := newJvmVersion("1.8.0_275")
+
+ otherVer := &Version{
+ Raw: "1.8.0_281",
+ Format: JVMFormat,
+ rich: rich{}, // jvmVersion will be nil
+ }
+
+ return thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty jvmVersion object",
+ },
+ {
+ name: "semantic format but empty semVer object",
+ setupFunc: func() (*jvmVersion, *Version) {
+ thisVer, _ := newJvmVersion("1.8.0_275")
+
+ otherVer := &Version{
+ Raw: "1.8.1",
+ Format: SemanticFormat,
+ rich: rich{}, // semVer will be nil
+ }
+
+ return thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty semVer object",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/kb_contraint.go b/grype/version/kb_contraint.go
index dc8d2e92365..c2367d7e95a 100644
--- a/grype/version/kb_contraint.go
+++ b/grype/version/kb_contraint.go
@@ -51,7 +51,7 @@ func (c kbConstraint) Satisfied(version *Version) (bool, error) {
}
if !c.supported(version.Format) {
- return false, fmt.Errorf("(kb) unsupported format: %s", version.Format)
+ return false, NewUnsupportedFormatError(KBFormat, version.Format)
}
return c.expression.satisfied(version)
diff --git a/grype/version/kb_version.go b/grype/version/kb_version.go
index 5b41d5910b1..37074c115bf 100644
--- a/grype/version/kb_version.go
+++ b/grype/version/kb_version.go
@@ -17,8 +17,9 @@ func newKBVersion(raw string) kbVersion {
}
func (v *kbVersion) Compare(other *Version) (int, error) {
- if other.Format != KBFormat {
- return -1, fmt.Errorf("unable to compare kb to given format: %s", other.Format)
+ other, err := finalizeComparisonVersion(other, KBFormat)
+ if err != nil {
+ return -1, err
}
if other.rich.kbVer == nil {
diff --git a/grype/version/kb_version_test.go b/grype/version/kb_version_test.go
new file mode 100644
index 00000000000..b379c999614
--- /dev/null
+++ b/grype/version/kb_version_test.go
@@ -0,0 +1,114 @@
+package version
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestKbVersionCompare(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "same format successful comparison",
+ thisVersion: "KB4562562",
+ otherVersion: "KB4562563",
+ otherFormat: KBFormat,
+ expectError: false,
+ },
+ {
+ name: "different format returns error",
+ thisVersion: "KB4562562",
+ otherVersion: "1.2.3",
+ otherFormat: SemanticFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "unknown format attempts upgrade - valid kb format",
+ thisVersion: "KB4562562",
+ otherVersion: "KB4562563",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer := newKBVersion(test.thisVersion)
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ require.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestKbVersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (*kbVersion, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (*kbVersion, *Version) {
+ thisVer := newKBVersion("KB4562562")
+ return &thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "empty kbVersion in other object",
+ setupFunc: func() (*kbVersion, *Version) {
+ thisVer := newKBVersion("KB4562562")
+
+ otherVer := &Version{
+ Raw: "KB4562563",
+ Format: KBFormat,
+ rich: rich{},
+ }
+
+ return &thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty kbVersion object",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/maven_constraint.go b/grype/version/maven_constraint.go
new file mode 100644
index 00000000000..77c4e95ec2f
--- /dev/null
+++ b/grype/version/maven_constraint.go
@@ -0,0 +1,72 @@
+package version
+
+import "fmt"
+
+type mavenConstraint struct {
+ raw string
+ expression constraintExpression
+}
+
+func newMavenConstraint(raw string) (mavenConstraint, error) {
+ if raw == "" {
+ // empty constraints are always satisfied
+ return mavenConstraint{}, nil
+ }
+
+ constraints, err := newConstraintExpression(raw, newMavenComparator)
+ if err != nil {
+ return mavenConstraint{}, fmt.Errorf("unable to parse maven constraint phrase: %w", err)
+ }
+
+ return mavenConstraint{
+ raw: raw,
+ expression: constraints,
+ }, nil
+}
+
+func newMavenComparator(unit constraintUnit) (Comparator, error) {
+ ver, err := newMavenVersion(unit.version)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse constraint version (%s): %w", unit.version, err)
+ }
+
+ return ver, nil
+}
+
+func (c mavenConstraint) supported(format Format) bool {
+ return format == MavenFormat
+}
+
+func (c mavenConstraint) Satisfied(version *Version) (satisfied bool, err error) {
+ if c.raw == "" && version != nil {
+ // empty constraints are always satisfied
+ return true, nil
+ }
+
+ if version == nil {
+ if c.raw != "" {
+ // a non-empty constraint with no version given should always fail
+ return false, nil
+ }
+
+ return true, nil
+ }
+
+ if !c.supported(version.Format) {
+ return false, NewUnsupportedFormatError(MavenFormat, version.Format)
+ }
+
+ if version.rich.mavenVer == nil {
+ return false, fmt.Errorf("no rich apk version given: %+v", version)
+ }
+
+ return c.expression.satisfied(version)
+}
+
+func (c mavenConstraint) String() string {
+ if c.raw == "" {
+ return "none (maven)"
+ }
+
+ return fmt.Sprintf("%s (maven)", c.raw)
+}
diff --git a/grype/version/maven_constraint_test.go b/grype/version/maven_constraint_test.go
new file mode 100644
index 00000000000..ff5a9888997
--- /dev/null
+++ b/grype/version/maven_constraint_test.go
@@ -0,0 +1,106 @@
+package version
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestVersionConstraintJava(t *testing.T) {
+ tests := []testCase{
+ {version: "1", constraint: "< 2.5", satisfied: true},
+ {version: "1.0", constraint: "< 1.1", satisfied: true},
+ {version: "1.1", constraint: "< 1.2", satisfied: true},
+ {version: "1.0.0", constraint: "< 1.1", satisfied: true},
+ {version: "1.0.1", constraint: "< 1.1", satisfied: true},
+ {version: "1.1", constraint: "> 1.2.0", satisfied: false},
+ {version: "1.0-alpha-1", constraint: "> 1.0", satisfied: false},
+ {version: "1.0-alpha-1", constraint: "> 1.0-alpha-2", satisfied: false},
+ {version: "1.0-alpha-1", constraint: "< 1.0-beta-1", satisfied: true},
+ {version: "1.0-beta-1", constraint: "< 1.0-SNAPSHOT", satisfied: true},
+ {version: "1.0-SNAPSHOT", constraint: "< 1.0", satisfied: true},
+ {version: "1.0-alpha-1-SNAPSHOT", constraint: "> 1.0-alpha-1", satisfied: false},
+ {version: "1.0", constraint: "< 1.0-1", satisfied: true},
+ {version: "1.0-1", constraint: "< 1.0-2", satisfied: true},
+ {version: "1.0.0", constraint: "< 1.0-1", satisfied: true},
+ {version: "2.0-1", constraint: "> 2.0.1", satisfied: false},
+ {version: "2.0.1-klm", constraint: "> 2.0.1-lmn", satisfied: false},
+ {version: "2.0.1", constraint: "< 2.0.1-xyz", satisfied: true},
+ {version: "2.0.1", constraint: "< 2.0.1-123", satisfied: true},
+ {version: "2.0.1-xyz", constraint: "< 2.0.1-123", satisfied: true},
+ {version: "2.414.2-cb-5", constraint: "> 2.414.2", satisfied: true},
+ {version: "5.2.25.RELEASE", constraint: "< 5.2.25", satisfied: false},
+ {version: "5.2.25.RELEASE", constraint: "<= 5.2.25", satisfied: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ constraint, err := newMavenConstraint(test.constraint)
+
+ assert.NoError(t, err, "unexpected error from newMavenConstraint %s: %v", test.version, err)
+ test.assertVersionConstraint(t, MavenFormat, constraint)
+
+ })
+ }
+}
+
+func TestVersionEqualityJava(t *testing.T) {
+ tests := []testCase{
+ {version: "1", constraint: "1", satisfied: true},
+ {version: "1", constraint: "1.0", satisfied: true},
+ {version: "1", constraint: "1.0.0", satisfied: true},
+ {version: "1.0", constraint: "1.0.0", satisfied: true},
+ {version: "1", constraint: "1-0", satisfied: true},
+ {version: "1", constraint: "1.0-0", satisfied: true},
+ {version: "1.0", constraint: "1.0-0", satisfied: true},
+ {version: "1a", constraint: "1-a", satisfied: true},
+ {version: "1a", constraint: "1.0-a", satisfied: true},
+ {version: "1a", constraint: "1.0.0-a", satisfied: true},
+ {version: "1.0a", constraint: "1-a", satisfied: true},
+ {version: "1.0.0a", constraint: "1-a", satisfied: true},
+ {version: "1x", constraint: "1-x", satisfied: true},
+ {version: "1x", constraint: "1.0-x", satisfied: true},
+ {version: "1x", constraint: "1.0.0-x", satisfied: true},
+ {version: "1.0x", constraint: "1-x", satisfied: true},
+ {version: "1.0.0x", constraint: "1-x", satisfied: true},
+ {version: "1ga", constraint: "1", satisfied: true},
+ {version: "1release", constraint: "1", satisfied: true},
+ {version: "1final", constraint: "1", satisfied: true},
+ {version: "1cr", constraint: "1rc", satisfied: true},
+ {version: "1a1", constraint: "1-alpha-1", satisfied: true},
+ {version: "1b2", constraint: "1-beta-2", satisfied: true},
+ {version: "1m3", constraint: "1-milestone-3", satisfied: true},
+ {version: "1X", constraint: "1x", satisfied: true},
+ {version: "1A", constraint: "1a", satisfied: true},
+ {version: "1B", constraint: "1b", satisfied: true},
+ {version: "1M", constraint: "1m", satisfied: true},
+ {version: "1Ga", constraint: "1", satisfied: true},
+ {version: "1GA", constraint: "1", satisfied: true},
+ {version: "1RELEASE", constraint: "1", satisfied: true},
+ {version: "1release", constraint: "1", satisfied: true},
+ {version: "1RELeaSE", constraint: "1", satisfied: true},
+ {version: "1Final", constraint: "1", satisfied: true},
+ {version: "1FinaL", constraint: "1", satisfied: true},
+ {version: "1FINAL", constraint: "1", satisfied: true},
+ {version: "1Cr", constraint: "1Rc", satisfied: true},
+ {version: "1cR", constraint: "1rC", satisfied: true},
+ {version: "1m3", constraint: "1Milestone3", satisfied: true},
+ {version: "1m3", constraint: "1MileStone3", satisfied: true},
+ {version: "1m3", constraint: "1MILESTONE3", satisfied: true},
+ {version: "1", constraint: "01", satisfied: true},
+ {version: "1", constraint: "001", satisfied: true},
+ {version: "1.1", constraint: "1.01", satisfied: true},
+ {version: "1.1", constraint: "1.001", satisfied: true},
+ {version: "1-1", constraint: "1-01", satisfied: true},
+ {version: "1-1", constraint: "1-001", satisfied: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ constraint, err := newMavenConstraint(test.constraint)
+
+ assert.NoError(t, err, "unexpected error from newMavenConstraint %s: %v", test.version, err)
+ test.assertVersionConstraint(t, MavenFormat, constraint)
+ })
+ }
+}
diff --git a/grype/version/maven_version.go b/grype/version/maven_version.go
new file mode 100644
index 00000000000..916cf1fe363
--- /dev/null
+++ b/grype/version/maven_version.go
@@ -0,0 +1,52 @@
+package version
+
+import (
+ "fmt"
+
+ mvnv "github.com/masahiro331/go-mvn-version"
+)
+
+type mavenVersion struct {
+ raw string
+ version mvnv.Version
+}
+
+func newMavenVersion(raw string) (*mavenVersion, error) {
+ ver, err := mvnv.NewVersion(raw)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate new java version from: %s; %w", raw, err)
+ }
+
+ return &mavenVersion{
+ raw: raw,
+ version: ver,
+ }, nil
+}
+
+// Compare returns 0 if other == j, 1 if other > j, and -1 if other < j.
+// If an error is returned, the int value is -1
+func (j *mavenVersion) Compare(other *Version) (int, error) {
+ other, err := finalizeComparisonVersion(other, MavenFormat)
+ if err != nil {
+ return -1, err
+ }
+ if other.rich.mavenVer == nil {
+ return -1, fmt.Errorf("given empty mavenVersion object")
+ }
+
+ submittedVersion := other.rich.mavenVer.version
+ if submittedVersion.Equal(j.version) {
+ return 0, nil
+ }
+ if submittedVersion.LessThan(j.version) {
+ return -1, nil
+ }
+ if submittedVersion.GreaterThan(j.version) {
+ return 1, nil
+ }
+
+ return -1, fmt.Errorf(
+ "could not compare java versions: %v with %v",
+ submittedVersion.String(),
+ j.version.String())
+}
diff --git a/grype/version/maven_version_test.go b/grype/version/maven_version_test.go
new file mode 100644
index 00000000000..633cea0de59
--- /dev/null
+++ b/grype/version/maven_version_test.go
@@ -0,0 +1,225 @@
+package version
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_javaVersion_Compare(t *testing.T) {
+ tests := []struct {
+ name string
+ compare string
+ want int
+ }{
+ {
+ name: "1",
+ compare: "2",
+ want: -1,
+ },
+ {
+ name: "1.8.0_282",
+ compare: "1.8.0_282",
+ want: 0,
+ },
+ {
+ name: "2.5",
+ compare: "2.0",
+ want: 1,
+ },
+ {
+ name: "2.414.2-cb-5",
+ compare: "2.414.2",
+ want: 1,
+ },
+ {
+ name: "5.2.25.RELEASE", // see https://mvnrepository.com/artifact/org.springframework/spring-web
+ compare: "5.2.25",
+ want: 0,
+ },
+ {
+ name: "5.2.25.release",
+ compare: "5.2.25",
+ want: 0,
+ },
+ {
+ name: "5.2.25.FINAL",
+ compare: "5.2.25",
+ want: 0,
+ },
+ {
+ name: "5.2.25.final",
+ compare: "5.2.25",
+ want: 0,
+ },
+ {
+ name: "5.2.25.GA",
+ compare: "5.2.25",
+ want: 0,
+ },
+ {
+ name: "5.2.25.ga",
+ compare: "5.2.25",
+ want: 0,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ j, err := NewVersion(tt.name, MavenFormat)
+ assert.NoError(t, err)
+
+ j2, err := NewVersion(tt.compare, MavenFormat)
+ assert.NoError(t, err)
+
+ if got, _ := j2.rich.mavenVer.Compare(j); got != tt.want {
+ t.Errorf("Compare() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestMavenVersionCompare_Format(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "same format successful comparison",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: MavenFormat,
+ expectError: false,
+ },
+ {
+ name: "same format successful comparison with qualifiers",
+ thisVersion: "1.2.3-SNAPSHOT",
+ otherVersion: "1.2.3-RELEASE",
+ otherFormat: MavenFormat,
+ expectError: false,
+ },
+ {
+ name: "different format returns error",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3",
+ otherFormat: SemanticFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - apk",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3-r4",
+ otherFormat: ApkFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "unknown format attempts upgrade - valid maven format",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, err := newMavenVersion(test.thisVersion)
+ require.NoError(t, err)
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ require.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestMavenVersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (*mavenVersion, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (*mavenVersion, *Version) {
+ thisVer, _ := newMavenVersion("1.2.3")
+ return thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "empty mavenVersion in other object",
+ setupFunc: func() (*mavenVersion, *Version) {
+ thisVer, _ := newMavenVersion("1.2.3")
+
+ otherVer := &Version{
+ Raw: "1.2.4",
+ Format: MavenFormat,
+ rich: rich{},
+ }
+
+ return thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty mavenVersion object",
+ },
+ {
+ name: "incomparable maven versions",
+ setupFunc: func() (*mavenVersion, *Version) {
+ // This test would be hard to construct in practice since the Maven
+ // version library handles most comparisons, but we can simulate the
+ // error condition by creating a mock that would trigger the last
+ // error condition in the Compare function
+ thisVer, _ := newMavenVersion("1.2.3")
+
+ // We'd need to modify the otherVer manually to create a scenario
+ // where none of the comparison methods return true, which is unlikely
+ // in real usage but could be simulated for test coverage
+ otherVer, _ := NewVersion("1.2.4", MavenFormat)
+
+ return thisVer, otherVer
+ },
+ expectError: false, // Changed to false since we can't easily trigger the last error condition
+ errorSubstring: "could not compare java versions",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/pep440_constraint.go b/grype/version/pep440_constraint.go
new file mode 100644
index 00000000000..4ea4c479355
--- /dev/null
+++ b/grype/version/pep440_constraint.go
@@ -0,0 +1,62 @@
+package version
+
+import "fmt"
+
+type pep440Constraint struct {
+ raw string
+ expression constraintExpression
+}
+
+func (p pep440Constraint) String() string {
+ if p.raw == "" {
+ return "none (python)"
+ }
+ return fmt.Sprintf("%s (python)", p.raw)
+}
+
+func (p pep440Constraint) Satisfied(version *Version) (bool, error) {
+ if p.raw == "" && version != nil {
+ // an empty constraint is always satisfied
+ return true, nil
+ } else if version == nil {
+ if p.raw != "" {
+ // a non-empty constraint with no version given should always fail
+ return false, nil
+ }
+ return true, nil
+ }
+ if version.Format != PythonFormat {
+ return false, NewUnsupportedFormatError(PythonFormat, version.Format)
+ }
+
+ if version.rich.pep440version == nil {
+ return false, fmt.Errorf("no rich PEP440 version given: %+v", version)
+ }
+ return p.expression.satisfied(version)
+}
+
+var _ Constraint = (*pep440Constraint)(nil)
+
+func newPep440Constraint(raw string) (pep440Constraint, error) {
+ if raw == "" {
+ return pep440Constraint{}, nil
+ }
+
+ constraints, err := newConstraintExpression(raw, newPep440Comparator)
+ if err != nil {
+ return pep440Constraint{}, fmt.Errorf("unable to parse pep440 constraint phrase %w", err)
+ }
+
+ return pep440Constraint{
+ expression: constraints,
+ raw: raw,
+ }, nil
+}
+
+func newPep440Comparator(unit constraintUnit) (Comparator, error) {
+ ver, err := newPep440Version(unit.version)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse constraint version (%s): %w", unit.version, err)
+ }
+ return ver, nil
+}
diff --git a/grype/version/pep440_constraint_test.go b/grype/version/pep440_constraint_test.go
new file mode 100644
index 00000000000..718145bebdf
--- /dev/null
+++ b/grype/version/pep440_constraint_test.go
@@ -0,0 +1,221 @@
+package version
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestItWorks(t *testing.T) {
+ tests := []testCase{
+ {
+ name: "empty constraint",
+ version: "2.3.1",
+ constraint: "",
+ satisfied: true,
+ },
+ {
+ name: "version range within",
+ constraint: ">1.0, <2.0",
+ version: "1.2+beta-3",
+ satisfied: true,
+ },
+ {
+ name: "version within compound range",
+ constraint: ">1.0, <2.0 || > 3.0",
+ version: "3.2+beta-3",
+ satisfied: true,
+ },
+ {
+ name: "version within compound range (2)",
+ constraint: ">1.0, <2.0 || > 3.0",
+ version: "1.2+beta-3",
+ satisfied: true,
+ },
+ {
+ name: "version not within compound range",
+ constraint: ">1.0, <2.0 || > 3.0",
+ version: "2.2+beta-3",
+ satisfied: false,
+ },
+ {
+ name: "version range outside (right)",
+ constraint: ">1.0, <2.0",
+ version: "2.1-beta-3",
+ satisfied: false,
+ },
+ {
+ name: "version range outside (left)",
+ constraint: ">1.0, <2.0",
+ version: "0.9-beta-2",
+ satisfied: false,
+ },
+ {
+ name: "version range within (excluding left, prerelease)",
+ constraint: ">=1.0, <2.0",
+ version: "1.0-beta-3",
+ satisfied: false,
+ },
+ {
+ name: "version range within (including left)",
+ constraint: ">=1.1, <2.0",
+ version: "1.1",
+ satisfied: true,
+ },
+ {
+ name: "version range within (excluding right, 1)",
+ constraint: ">1.0, <=2.0",
+ version: "2.0-beta-3",
+ satisfied: true,
+ },
+ {
+ name: "version range within (excluding right, 2)",
+ constraint: ">1.0, <2.0",
+ version: "2.0-beta-3",
+ satisfied: true,
+ },
+ {
+ name: "version range within (including right)",
+ constraint: ">1.0, <=2.0",
+ version: "2.0",
+ satisfied: true,
+ },
+ {
+ name: "version range within (including right, longer version [valid semver, bad fuzzy])",
+ constraint: ">1.0, <=2.0",
+ version: "2.0.0",
+ satisfied: true,
+ },
+ {
+ name: "bad semver (eq)",
+ version: "5a2",
+ constraint: "=5a2",
+ satisfied: true,
+ },
+ {
+ name: "bad semver (gt)",
+ version: "5a2",
+ constraint: ">5a1",
+ satisfied: true,
+ },
+ {
+ name: "bad semver (lt)",
+ version: "5a2",
+ constraint: "<6a1",
+ satisfied: true,
+ },
+ {
+ name: "bad semver (lte)",
+ version: "5a2",
+ constraint: "<=5a2",
+ satisfied: true,
+ },
+ {
+ name: "bad semver (gte)",
+ version: "5a2",
+ constraint: ">=5a2",
+ satisfied: true,
+ },
+ {
+ name: "bad semver (lt boundary)",
+ version: "5a2",
+ constraint: "<5a2",
+ satisfied: false,
+ },
+ // regression for https://github.com/anchore/go-version/pull/2
+ {
+ name: "indirect package match",
+ version: "1.3.2-r0",
+ constraint: "<= 1.3.3-r0",
+ satisfied: true,
+ },
+ {
+ name: "indirect package no match",
+ version: "1.3.4-r0",
+ constraint: "<= 1.3.3-r0",
+ satisfied: false,
+ },
+ {
+ name: "vulndb fuzzy constraint single quoted",
+ version: "4.5.2",
+ constraint: "'4.5.1' || '4.5.2'",
+ satisfied: true,
+ },
+ {
+ name: "vulndb fuzzy constraint double quoted",
+ version: "4.5.2",
+ constraint: "\"4.5.1\" || \"4.5.2\"",
+ satisfied: true,
+ },
+ {
+ name: "rc candidates with no '-' can match semver pattern",
+ version: "1.20rc1",
+ constraint: " = 1.20.0-rc1",
+ satisfied: true,
+ },
+ {
+ name: "candidates ahead of alpha",
+ version: "3.11.0",
+ constraint: "> 3.11.0-alpha1",
+ satisfied: true,
+ },
+ {
+ name: "candidates ahead of beta",
+ version: "3.11.0",
+ constraint: "> 3.11.0-beta1",
+ satisfied: true,
+ },
+ {
+ name: "candidates ahead of same alpha versions",
+ version: "3.11.0-alpha5",
+ constraint: "> 3.11.0-alpha1",
+ satisfied: true,
+ },
+ {
+ name: "candidates are placed correctly between alpha and release",
+ version: "3.11.0-beta5",
+ constraint: "3.11.0 || = 3.11.0-alpha1",
+ satisfied: false,
+ },
+ {
+ name: "candidates with pre suffix are sorted numerically",
+ version: "1.0.2pre1",
+ constraint: " < 1.0.2pre2",
+ satisfied: true,
+ },
+ {
+ name: "openssl pre2 is still considered less than release",
+ version: "1.1.1-pre2",
+ constraint: "> 1.1.1-pre1, < 1.1.1",
+ satisfied: true,
+ },
+ {
+ name: "major version releases are less than their subsequent patch releases with letter suffixes",
+ version: "1.1.1",
+ constraint: "> 1.1.1-a",
+ satisfied: true,
+ },
+ {
+ name: "date based pep440 version string boundary condition",
+ version: "2022.12.7",
+ constraint: ">=2017.11.05,<2022.12.07",
+ },
+ {
+ name: "certifi false positive is fixed",
+ version: "2022.12.7",
+ constraint: ">=2017.11.05,<2022.12.07",
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ c, err := newPep440Constraint(tc.constraint)
+ require.NoError(t, err)
+ v, err := NewVersion(tc.version, PythonFormat)
+ require.NoError(t, err)
+ sat, err := c.Satisfied(v)
+ require.NoError(t, err)
+ assert.Equal(t, tc.satisfied, sat)
+ })
+ }
+}
diff --git a/grype/version/pep440_version.go b/grype/version/pep440_version.go
new file mode 100644
index 00000000000..2c9ec696d9d
--- /dev/null
+++ b/grype/version/pep440_version.go
@@ -0,0 +1,36 @@
+package version
+
+import (
+ "fmt"
+
+ goPepVersion "github.com/aquasecurity/go-pep440-version"
+)
+
+var _ Comparator = (*pep440Version)(nil)
+
+type pep440Version struct {
+ obj goPepVersion.Version
+}
+
+func (p pep440Version) Compare(other *Version) (int, error) {
+ other, err := finalizeComparisonVersion(other, PythonFormat)
+ if err != nil {
+ return -1, err
+ }
+
+ if other.rich.pep440version == nil {
+ return -1, fmt.Errorf("given empty pep440 object")
+ }
+
+ return other.rich.pep440version.obj.Compare(p.obj), nil
+}
+
+func newPep440Version(raw string) (pep440Version, error) {
+ parsed, err := goPepVersion.Parse(raw)
+ if err != nil {
+ return pep440Version{}, fmt.Errorf("could not parse pep440 version: %w", err)
+ }
+ return pep440Version{
+ obj: parsed,
+ }, nil
+}
diff --git a/grype/version/pep440_version_test.go b/grype/version/pep440_version_test.go
new file mode 100644
index 00000000000..3cb69f85d06
--- /dev/null
+++ b/grype/version/pep440_version_test.go
@@ -0,0 +1,138 @@
+package version
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestPep440VersionCompare(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "same format successful comparison",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: PythonFormat,
+ expectError: false,
+ },
+ {
+ name: "same format successful comparison with pre-release",
+ thisVersion: "1.2.3a1",
+ otherVersion: "1.2.3b2",
+ otherFormat: PythonFormat,
+ expectError: false,
+ },
+ {
+ name: "different format returns error",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3",
+ otherFormat: SemanticFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - apk",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3-r4",
+ otherFormat: ApkFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "unknown format attempts upgrade - valid python format",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ {
+ name: "unknown format attempts upgrade - invalid python format",
+ thisVersion: "1.2.3",
+ otherVersion: "not/valid/python-format",
+ otherFormat: UnknownFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, err := newPep440Version(test.thisVersion)
+ require.NoError(t, err)
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ require.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestPep440VersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (pep440Version, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (pep440Version, *Version) {
+ thisVer, _ := newPep440Version("1.2.3")
+ return thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "empty pep440version in other object",
+ setupFunc: func() (pep440Version, *Version) {
+ thisVer, _ := newPep440Version("1.2.3")
+
+ otherVer := &Version{
+ Raw: "1.2.4",
+ Format: PythonFormat,
+ rich: rich{},
+ }
+
+ return thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty pep440 object",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/portage_constraint.go b/grype/version/portage_constraint.go
index dd34d5e587b..d04dc56b056 100644
--- a/grype/version/portage_constraint.go
+++ b/grype/version/portage_constraint.go
@@ -48,7 +48,7 @@ func (c portageConstraint) Satisfied(version *Version) (bool, error) {
}
if !c.supported(version.Format) {
- return false, fmt.Errorf("(portage) unsupported format: %s", version.Format)
+ return false, NewUnsupportedFormatError(PortageFormat, version.Format)
}
if version.rich.portVer == nil {
diff --git a/grype/version/portage_version.go b/grype/version/portage_version.go
index 4b1fa950437..067b4756586 100644
--- a/grype/version/portage_version.go
+++ b/grype/version/portage_version.go
@@ -18,9 +18,11 @@ func newPortageVersion(raw string) portageVersion {
}
func (v *portageVersion) Compare(other *Version) (int, error) {
- if other.Format != PortageFormat {
- return -1, fmt.Errorf("unable to compare portage to given format: %s", other.Format)
+ other, err := finalizeComparisonVersion(other, PortageFormat)
+ if err != nil {
+ return -1, err
}
+
if other.rich.portVer == nil {
return -1, fmt.Errorf("given empty portageVersion object")
}
@@ -175,7 +177,7 @@ func comparePortageVersions(a, b string) int {
}
r2 := big.NewInt(0)
if match2[9] != "" {
- r1.SetString(match2[9], 10)
+ r2.SetString(match2[9], 10)
}
return r1.Cmp(r2)
diff --git a/grype/version/portage_version_test.go b/grype/version/portage_version_test.go
index c601fc09088..1659d2bce41 100644
--- a/grype/version/portage_version_test.go
+++ b/grype/version/portage_version_test.go
@@ -1,7 +1,11 @@
package version
import (
+ "strings"
"testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestVersionPortage(t *testing.T) {
@@ -18,6 +22,8 @@ func TestVersionPortage(t *testing.T) {
{"1_p1", "1_p0", 1},
{"1_p0", "1", 1},
{"1-r1", "1", 1},
+ {"1.2.3-r2", "1.2.3-r1", 1},
+ {"1.2.3-r1", "1.2.3-r2", -1},
}
for _, test := range tests {
@@ -34,3 +40,123 @@ func TestVersionPortage(t *testing.T) {
})
}
}
+
+func TestPortageVersionCompare_Format(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "same format successful comparison",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: PortageFormat,
+ expectError: false,
+ },
+ {
+ name: "same format successful comparison with suffixes",
+ thisVersion: "1.2.3-r1",
+ otherVersion: "1.2.3-r2",
+ otherFormat: PortageFormat,
+ expectError: false,
+ },
+ {
+ name: "different format returns error",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3",
+ otherFormat: SemanticFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - apk",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3-r4",
+ otherFormat: ApkFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "unknown format attempts upgrade - valid portage format",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer := newPortageVersion(test.thisVersion)
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ require.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestPortageVersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (*portageVersion, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (*portageVersion, *Version) {
+ thisVer := newPortageVersion("1.2.3")
+ return &thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "empty portageVersion in other object",
+ setupFunc: func() (*portageVersion, *Version) {
+ thisVer := newPortageVersion("1.2.3")
+
+ otherVer := &Version{
+ Raw: "1.2.4",
+ Format: PortageFormat,
+ rich: rich{},
+ }
+
+ return &thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty portageVersion object",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/rpm_constraint.go b/grype/version/rpm_constraint.go
index 0565f3e865b..4973139ba20 100644
--- a/grype/version/rpm_constraint.go
+++ b/grype/version/rpm_constraint.go
@@ -51,7 +51,7 @@ func (c rpmConstraint) Satisfied(version *Version) (bool, error) {
}
if !c.supported(version.Format) {
- return false, fmt.Errorf("(rpm) unsupported format: %s", version.Format)
+ return false, NewUnsupportedFormatError(RpmFormat, version.Format)
}
if version.rich.rpmVer == nil {
diff --git a/grype/version/rpm_constraint_test.go b/grype/version/rpm_constraint_test.go
index 890fac21579..77139361d44 100644
--- a/grype/version/rpm_constraint_test.go
+++ b/grype/version/rpm_constraint_test.go
@@ -60,6 +60,13 @@ func TestVersionRpmConstraint(t *testing.T) {
{version: "4.19.0-1.el7_5", constraint: "<= 12:4.19.0-1.el7", satisfied: false},
{version: "3:4.19.0-1.el7_5", constraint: "< 4.21.0-1.el7", satisfied: true},
{version: "4:1.2.3-3-el7_5", constraint: "< 1.2.3-el7_5~snapshot1", satisfied: false},
+ // regression https://github.com/anchore/grype/issues/398
+ {version: "8.3.1-5.el8.4", constraint: "< 0:8.3.1-5.el8.5", satisfied: true},
+ {version: "8.3.1-5.el8.40", constraint: "< 0:8.3.1-5.el8.5", satisfied: false},
+ {version: "8.3.1-5.el8", constraint: "< 0:8.3.1-5.el8.0.0", satisfied: false},
+ {version: "8.3.1-5.el8", constraint: "<= 0:8.3.1-5.el8.0.0", satisfied: true},
+ {version: "8.3.1-5.el8.0.0", constraint: "> 0:8.3.1-5.el8", satisfied: false},
+ {version: "8.3.1-5.el8.0.0", constraint: ">= 0:8.3.1-5.el8", satisfied: true},
}
for _, test := range tests {
diff --git a/grype/version/rpm_version.go b/grype/version/rpm_version.go
index 430b5e037f7..ecbedfe5638 100644
--- a/grype/version/rpm_version.go
+++ b/grype/version/rpm_version.go
@@ -2,7 +2,6 @@ package version
import (
"fmt"
- "math"
"reflect"
"regexp"
"strconv"
@@ -65,9 +64,11 @@ func splitEpochFromVersion(rawVersion string) (*int, string, error) {
}
func (v *rpmVersion) Compare(other *Version) (int, error) {
- if other.Format != RpmFormat {
- return -1, fmt.Errorf("unable to compare rpm to given format: %s", other.Format)
+ other, err := finalizeComparisonVersion(other, RpmFormat)
+ if err != nil {
+ return -1, err
}
+
if other.rich.rpmVer == nil {
return -1, fmt.Errorf("given empty rpmVersion object")
}
@@ -151,10 +152,11 @@ func compareRpmVersions(a, b string) int {
// get alpha/numeric segments
segsa := alphanumPattern.FindAllString(a, -1)
segsb := alphanumPattern.FindAllString(b, -1)
- segs := int(math.Min(float64(len(segsa)), float64(len(segsb))))
+ maxSegs := max(len(segsa), len(segsb))
+ minSegs := min(len(segsa), len(segsb))
// compare each segment
- for i := 0; i < segs; i++ {
+ for i := 0; i < minSegs; i++ {
a := segsa[i]
b := segsb[i]
@@ -204,11 +206,26 @@ func compareRpmVersions(a, b string) int {
}
// If there is a tilde in a segment past the min number of segments, find it.
- if len(segsa) > segs && []rune(segsa[segs])[0] == '~' {
+ if len(segsa) > minSegs && []rune(segsa[minSegs])[0] == '~' {
return -1
- } else if len(segsb) > segs && []rune(segsb[segs])[0] == '~' {
+ } else if len(segsb) > minSegs && []rune(segsb[minSegs])[0] == '~' {
return 1
}
+ // are the remaining segments 0s?
+ segaAll0s := true
+ segbAll0s := true
+ for i := minSegs; i < maxSegs; i++ {
+ if i < len(segsa) && segsa[i] != "0" {
+ segaAll0s = false
+ }
+ if i < len(segsb) && segsb[i] != "0" {
+ segbAll0s = false
+ }
+ }
+
+ if segaAll0s && segbAll0s {
+ return 0
+ }
// whoever has the most segments wins
if len(segsa) > len(segsb) {
diff --git a/grype/version/rpm_version_test.go b/grype/version/rpm_version_test.go
index 4997b021ce8..32eef13ab07 100644
--- a/grype/version/rpm_version_test.go
+++ b/grype/version/rpm_version_test.go
@@ -1,7 +1,11 @@
package version
import (
+ "strings"
"testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestVersionRpm(t *testing.T) {
@@ -54,3 +58,124 @@ func TestVersionRpm(t *testing.T) {
})
}
}
+
+func TestRpmVersionCompare_Format(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "same format successful comparison",
+ thisVersion: "1.2.3-1",
+ otherVersion: "1.2.3-2",
+ otherFormat: RpmFormat,
+ expectError: false,
+ },
+ {
+ name: "same format successful comparison with epoch",
+ thisVersion: "1:1.2.3-1",
+ otherVersion: "1:1.2.3-2",
+ otherFormat: RpmFormat,
+ expectError: false,
+ },
+ {
+ name: "different format returns error",
+ thisVersion: "1.2.3-1",
+ otherVersion: "1.2.3",
+ otherFormat: SemanticFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - apk",
+ thisVersion: "1.2.3-1",
+ otherVersion: "1.2.3-r4",
+ otherFormat: ApkFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "unknown format attempts upgrade - valid rpm format",
+ thisVersion: "1.2.3-1",
+ otherVersion: "1.2.3-2",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, err := newRpmVersion(test.thisVersion)
+ require.NoError(t, err)
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ require.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestRpmVersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (*rpmVersion, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (*rpmVersion, *Version) {
+ thisVer, _ := newRpmVersion("1.2.3-1")
+ return &thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "empty rpmVersion in other object",
+ setupFunc: func() (*rpmVersion, *Version) {
+ thisVer, _ := newRpmVersion("1.2.3-1")
+
+ otherVer := &Version{
+ Raw: "1.2.3-2",
+ Format: RpmFormat,
+ rich: rich{},
+ }
+
+ return &thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty rpmVersion object",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/semantic_constraint.go b/grype/version/semantic_constraint.go
index 00fe9523580..69192dff630 100644
--- a/grype/version/semantic_constraint.go
+++ b/grype/version/semantic_constraint.go
@@ -56,7 +56,7 @@ func (c semanticConstraint) Satisfied(version *Version) (bool, error) {
}
if !c.supported(version.Format) {
- return false, fmt.Errorf("(semantic) unsupported format: %s", version.Format)
+ return false, NewUnsupportedFormatError(SemanticFormat, version.Format)
}
if version.rich.semVer == nil {
diff --git a/grype/version/semantic_constraint_test.go b/grype/version/semantic_constraint_test.go
index 42f36711c80..43dae2f2c42 100644
--- a/grype/version/semantic_constraint_test.go
+++ b/grype/version/semantic_constraint_test.go
@@ -70,6 +70,12 @@ func TestVersionSemantic(t *testing.T) {
{version: "1.0.0-beta.11", constraint: "< 1.0.0-rc.1", satisfied: true},
{version: "1.0.0-rc.1", constraint: "> 1.0.0", satisfied: false},
{version: "1.0.0-rc.1", constraint: "< 1.0.0", satisfied: true},
+ {version: "1.20rc1", constraint: " = 1.20.0-rc1", satisfied: true},
+ {version: "1.21rc2", constraint: " = 1.21.1", satisfied: false},
+ {version: "1.21rc2", constraint: " = 1.21", satisfied: false},
+ {version: "1.21rc2", constraint: " = 1.21-rc2", satisfied: true},
+ {version: "1.21rc2", constraint: " = 1.21.0-rc2", satisfied: true},
+ {version: "1.21rc2", constraint: " = 1.21.0rc2", satisfied: true},
{version: "1.0.0-alpha.1", constraint: "> 1.0.0-alpha.1", satisfied: false},
{version: "1.0.0-alpha.2", constraint: "> 1.0.0-alpha.1", satisfied: true},
{version: "1.2.0-beta", constraint: ">1.0, <2.0", satisfied: true},
diff --git a/grype/version/semantic_version.go b/grype/version/semantic_version.go
index 68e7acc9fca..d9d2383afe3 100644
--- a/grype/version/semantic_version.go
+++ b/grype/version/semantic_version.go
@@ -21,9 +21,11 @@ func newSemanticVersion(raw string) (*semanticVersion, error) {
}
func (v *semanticVersion) Compare(other *Version) (int, error) {
- if other.Format != SemanticFormat {
- return -1, fmt.Errorf("unable to compare semantic version to given format: %s", other.Format)
+ other, err := finalizeComparisonVersion(other, SemanticFormat)
+ if err != nil {
+ return -1, err
}
+
if other.rich.semVer == nil {
return -1, fmt.Errorf("given empty semanticVersion object")
}
diff --git a/grype/version/semantic_version_test.go b/grype/version/semantic_version_test.go
new file mode 100644
index 00000000000..81d518dcf6f
--- /dev/null
+++ b/grype/version/semantic_version_test.go
@@ -0,0 +1,153 @@
+package version
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSemanticVersionCompare_Format(t *testing.T) {
+ tests := []struct {
+ name string
+ thisVersion string
+ otherVersion string
+ otherFormat Format
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "same format successful comparison",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: SemanticFormat,
+ expectError: false,
+ },
+ {
+ name: "same format successful comparison with prerelease",
+ thisVersion: "1.2.3-alpha",
+ otherVersion: "1.2.3-beta",
+ otherFormat: SemanticFormat,
+ expectError: false,
+ },
+ {
+ name: "same format successful comparison with build metadata",
+ thisVersion: "1.2.3+build.1",
+ otherVersion: "1.2.3+build.2",
+ otherFormat: SemanticFormat,
+ expectError: false,
+ },
+ {
+ name: "different format returns error",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3-1",
+ otherFormat: DebFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - apk",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3-r4",
+ otherFormat: ApkFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "different format returns error - rpm",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.3-1",
+ otherFormat: RpmFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ {
+ name: "unknown format attempts upgrade - valid semantic format",
+ thisVersion: "1.2.3",
+ otherVersion: "1.2.4",
+ otherFormat: UnknownFormat,
+ expectError: false,
+ },
+ {
+ name: "unknown format attempts upgrade - invalid semantic format",
+ thisVersion: "1.2.3",
+ otherVersion: "not.valid.semver",
+ otherFormat: UnknownFormat,
+ expectError: true,
+ errorSubstring: "unsupported version format for comparison",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, err := newSemanticVersion(test.thisVersion)
+ require.NoError(t, err)
+
+ otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
+ require.NoError(t, err)
+
+ result, err := thisVer.Compare(otherVer)
+
+ if test.expectError {
+ require.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
+ }
+ })
+ }
+}
+
+func TestSemanticVersionCompareEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFunc func() (*semanticVersion, *Version)
+ expectError bool
+ errorSubstring string
+ }{
+ {
+ name: "nil version object",
+ setupFunc: func() (*semanticVersion, *Version) {
+ thisVer, _ := newSemanticVersion("1.2.3")
+ return thisVer, nil
+ },
+ expectError: true,
+ errorSubstring: "no version provided for comparison",
+ },
+ {
+ name: "empty semanticVersion in other object",
+ setupFunc: func() (*semanticVersion, *Version) {
+ thisVer, _ := newSemanticVersion("1.2.3")
+
+ otherVer := &Version{
+ Raw: "1.2.4",
+ Format: SemanticFormat,
+ rich: rich{},
+ }
+
+ return thisVer, otherVer
+ },
+ expectError: true,
+ errorSubstring: "given empty semanticVersion object",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ thisVer, otherVer := test.setupFunc()
+
+ _, err := thisVer.Compare(otherVer)
+
+ assert.Error(t, err)
+ if test.errorSubstring != "" {
+ assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
+ "Expected error to contain '%s', got: %v", test.errorSubstring, err)
+ }
+ })
+ }
+}
diff --git a/grype/version/version.go b/grype/version/version.go
index b11cda901c9..b8195940454 100644
--- a/grype/version/version.go
+++ b/grype/version/version.go
@@ -7,6 +7,10 @@ import (
"github.com/anchore/syft/syft/cpe"
)
+// ErrUnsupportedVersion is returned when a version string cannot be parsed into a rich version object
+// for a known unsupported case (e.g. golang "devel" version).
+var ErrUnsupportedVersion = fmt.Errorf("unsupported version value")
+
type Version struct {
Raw string
Format Format
@@ -14,13 +18,17 @@ type Version struct {
}
type rich struct {
- cpeVers []cpe.CPE
- semVer *semanticVersion
- apkVer *apkVersion
- debVer *debVersion
- rpmVer *rpmVersion
- kbVer *kbVersion
- portVer *portageVersion
+ cpeVers []cpe.CPE
+ semVer *semanticVersion
+ apkVer *apkVersion
+ debVer *debVersion
+ golangVersion *golangVersion
+ mavenVer *mavenVersion
+ rpmVer *rpmVersion
+ kbVer *kbVersion
+ portVer *portageVersion
+ pep440version *pep440Version
+ jvmVersion *jvmVersion
}
func NewVersion(raw string, format Format) (*Version, error) {
@@ -38,7 +46,9 @@ func NewVersion(raw string, format Format) (*Version, error) {
}
func NewVersionFromPkg(p pkg.Package) (*Version, error) {
- ver, err := NewVersion(p.Version, FormatFromPkgType(p.Type))
+ format := FormatFromPkg(p)
+
+ ver, err := NewVersion(p.Version, format)
if err != nil {
return nil, err
}
@@ -61,13 +71,22 @@ func (v *Version) populate() error {
ver, err := newDebVersion(v.Raw)
v.rich.debVer = ver
return err
+ case GolangFormat:
+ ver, err := newGolangVersion(v.Raw)
+ v.rich.golangVersion = ver
+ return err
+ case MavenFormat:
+ ver, err := newMavenVersion(v.Raw)
+ v.rich.mavenVer = ver
+ return err
case RpmFormat:
ver, err := newRpmVersion(v.Raw)
v.rich.rpmVer = &ver
return err
case PythonFormat:
- // use the fuzzy constraint
- return nil
+ ver, err := newPep440Version(v.Raw)
+ v.rich.pep440version = &ver
+ return err
case KBFormat:
ver := newKBVersion(v.Raw)
v.rich.kbVer = &ver
@@ -80,6 +99,10 @@ func (v *Version) populate() error {
ver := newPortageVersion(v.Raw)
v.rich.portVer = &ver
return nil
+ case JVMFormat:
+ ver, err := newJvmVersion(v.Raw)
+ v.rich.jvmVersion = ver
+ return err
case UnknownFormat:
// use the raw string + fuzzy constraint
return nil
@@ -95,3 +118,55 @@ func (v Version) CPEs() []cpe.CPE {
func (v Version) String() string {
return fmt.Sprintf("%s (%s)", v.Raw, v.Format)
}
+
+func (v Version) Compare(other *Version) (int, error) {
+ if other == nil {
+ return -1, ErrNoVersionProvided
+ }
+
+ if other.Format == v.Format {
+ return v.compareSameFormat(other)
+ }
+
+ // different formats, try to convert to a common format
+ common, err := finalizeComparisonVersion(other, v.Format)
+ if err != nil {
+ return -1, err
+ }
+
+ return v.compareSameFormat(common)
+}
+
+func (v Version) compareSameFormat(other *Version) (int, error) {
+ switch v.Format {
+ case SemanticFormat:
+ return v.rich.semVer.verObj.Compare(other.rich.semVer.verObj), nil
+ case ApkFormat:
+ return v.rich.apkVer.Compare(other)
+ case DebFormat:
+ return v.rich.debVer.Compare(other)
+ case GolangFormat:
+ return v.rich.golangVersion.Compare(other)
+ case MavenFormat:
+ return v.rich.mavenVer.Compare(other)
+ case RpmFormat:
+ return v.rich.rpmVer.Compare(other)
+ case PythonFormat:
+ return v.rich.pep440version.Compare(other)
+ case KBFormat:
+ return v.rich.kbVer.Compare(other)
+ case GemFormat:
+ return v.rich.semVer.verObj.Compare(other.rich.semVer.verObj), nil
+ case PortageFormat:
+ return v.rich.portVer.Compare(other)
+ case JVMFormat:
+ return v.rich.jvmVersion.Compare(other)
+ }
+
+ v1, err := newFuzzyVersion(v.Raw)
+ if err != nil {
+ return -1, fmt.Errorf("unable to parse version (%s) as a fuzzy version: %w", v.Raw, err)
+ }
+
+ return v1.Compare(other)
+}
diff --git a/grype/version/version_test.go b/grype/version/version_test.go
new file mode 100644
index 00000000000..fb59d2cbf79
--- /dev/null
+++ b/grype/version/version_test.go
@@ -0,0 +1,133 @@
+package version
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestVersionCompare(t *testing.T) {
+ tests := []struct {
+ name string
+ version1 string
+ version2 string
+ format Format
+ expectedResult int
+ expectErr bool
+ }{
+ {
+ name: "v1 greater than v2",
+ version1: "2.0.0",
+ version2: "1.0.0",
+ format: SemanticFormat,
+ expectedResult: 1,
+ expectErr: false,
+ },
+ {
+ name: "v1 less than v2",
+ version1: "1.0.0",
+ version2: "2.0.0",
+ format: SemanticFormat,
+ expectedResult: -1,
+ expectErr: false,
+ },
+ {
+ name: "v1 equal to v2",
+ version1: "1.0.0",
+ version2: "1.0.0",
+ format: SemanticFormat,
+ expectedResult: 0,
+ expectErr: false,
+ },
+ {
+ name: "compare with nil version",
+ version1: "1.0.0",
+ version2: "",
+ format: SemanticFormat,
+ expectedResult: -1,
+ expectErr: true,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ v1, err := NewVersion(tc.version1, tc.format)
+ require.NoError(t, err, "failed to create version1")
+
+ var v2 *Version
+ if tc.version2 == "" {
+ v2 = nil // test nil case
+ } else if tc.name == "different formats" {
+ // use a different format for the second version
+ v2, err = NewVersion(tc.version2, PythonFormat)
+ require.NoError(t, err, "failed to create version2 with different format")
+ } else {
+ v2, err = NewVersion(tc.version2, tc.format)
+ require.NoError(t, err, "failed to create version2")
+ }
+
+ result, err := v1.Compare(v2)
+
+ if tc.expectErr {
+ assert.Error(t, err, "expected an error but got none")
+ } else {
+ assert.NoError(t, err, "unexpected error during comparison")
+ assert.Equal(t, tc.expectedResult, result, "comparison result mismatch")
+ }
+ })
+ }
+}
+
+func Test_UpgradeUnknownRightSideComparison(t *testing.T) {
+ v1, err := NewVersion("1.0.0", SemanticFormat)
+ require.NoError(t, err)
+
+ // test if we can upgrade an unknown format to a known format when the left hand side is known
+ v2, err := NewVersion("1.0.0", UnknownFormat)
+ require.NoError(t, err)
+
+ result, err := v1.Compare(v2)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, result, "versions should be equal after format conversion")
+}
+
+func TestVersionCompareSameFormat(t *testing.T) {
+ formats := []struct {
+ name string
+ format Format
+ }{
+ {"Semantic", SemanticFormat},
+ {"APK", ApkFormat},
+ {"Deb", DebFormat},
+ {"Golang", GolangFormat},
+ {"Maven", MavenFormat},
+ {"RPM", RpmFormat},
+ {"Python", PythonFormat},
+ {"KB", KBFormat},
+ {"Gem", GemFormat},
+ {"Portage", PortageFormat},
+ {"JVM", JVMFormat},
+ {"Unknown", UnknownFormat},
+ }
+
+ for _, fmt := range formats {
+ t.Run(fmt.name, func(t *testing.T) {
+ // just test that we can create and compare versions of this format
+ // without errors - not testing the actual comparison logic
+ v1, err := NewVersion("1.0.0", fmt.format)
+ if err != nil {
+ t.Skipf("Skipping %s format, couldn't create version: %v", fmt.name, err)
+ }
+
+ v2, err := NewVersion("1.0.0", fmt.format)
+ if err != nil {
+ t.Skipf("Skipping %s format, couldn't create second version: %v", fmt.name, err)
+ }
+
+ result, err := v1.Compare(v2)
+ assert.NoError(t, err, "comparison error")
+ assert.Equal(t, 0, result, "equal versions should return 0")
+ })
+ }
+}
diff --git a/grype/vex/openvex/implementation.go b/grype/vex/openvex/implementation.go
new file mode 100644
index 00000000000..f5d3cef7497
--- /dev/null
+++ b/grype/vex/openvex/implementation.go
@@ -0,0 +1,341 @@
+package openvex
+
+import (
+ "errors"
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ openvex "github.com/openvex/go-vex/pkg/vex"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/packageurl-go"
+ "github.com/anchore/syft/syft/source"
+)
+
+type Processor struct{}
+
+func New() *Processor {
+ return &Processor{}
+}
+
+// Match captures the criteria that caused a vulnerability to match
+type Match struct {
+ Statement openvex.Statement
+}
+
+// SearchedBy captures the prameters used to search through the VEX data
+type SearchedBy struct {
+ Vulnerability string
+ Product string
+ Subcomponents []string
+}
+
+// augmentStatuses are the VEX statuses that augment results
+var augmentStatuses = []openvex.Status{
+ openvex.StatusAffected,
+ openvex.StatusUnderInvestigation,
+}
+
+// filterStatuses are the VEX statuses that filter matched to the ignore list
+var ignoreStatuses = []openvex.Status{
+ openvex.StatusNotAffected,
+ openvex.StatusFixed,
+}
+
+// ReadVexDocuments reads and merges VEX documents
+func (ovm *Processor) ReadVexDocuments(docs []string) (interface{}, error) {
+ // Combine all VEX documents into a single VEX document
+ vexdata, err := openvex.MergeFiles(docs)
+ if err != nil {
+ return nil, fmt.Errorf("merging vex documents: %w", err)
+ }
+
+ return vexdata, nil
+}
+
+// productIdentifiersFromContext reads the package context and returns software
+// identifiers identifying the scanned image.
+func productIdentifiersFromContext(pkgContext *pkg.Context) ([]string, error) {
+ switch v := pkgContext.Source.Metadata.(type) {
+ case source.ImageMetadata:
+ tagIdentifiers := identifiersFromTags(v.Tags, pkgContext.Source.Name)
+ digestIdentifiers := identifiersFromDigests(v.RepoDigests)
+ identifiers := slices.Concat(tagIdentifiers, digestIdentifiers)
+ return identifiers, nil
+ default:
+ // Fail for now
+ return nil, errors.New("source type not supported for VEX")
+ }
+}
+
+func identifiersFromTags(tags []string, name string) []string {
+ identifiers := []string{}
+
+ for _, tag := range tags {
+ identifiers = append(identifiers, tag)
+
+ tagMap := map[string]string{}
+ _, splitTag, found := strings.Cut(tag, ":")
+ if found {
+ tagMap["tag"] = splitTag
+ qualifiers := packageurl.QualifiersFromMap(tagMap)
+
+ identifiers = append(identifiers, packageurl.NewPackageURL("oci", "", name, "", qualifiers, "").String())
+ }
+ }
+
+ return identifiers
+}
+
+func identifiersFromDigests(digests []string) []string {
+ identifiers := []string{}
+
+ for _, d := range digests {
+ // The first identifier is the original image reference:
+ identifiers = append(identifiers, d)
+
+ // Not an image reference, skip
+ ref, err := name.ParseReference(d)
+ if err != nil {
+ continue
+ }
+
+ var repoURL string
+ shaString := ref.Identifier()
+
+ // If not a digest, we can't form a purl, so skip it
+ if !strings.HasPrefix(shaString, "sha256:") {
+ continue
+ }
+
+ pts := strings.Split(ref.Context().RepositoryStr(), "/")
+ name := pts[len(pts)-1]
+ repoURL = strings.TrimSuffix(
+ ref.Context().RegistryStr()+"/"+ref.Context().RepositoryStr(),
+ fmt.Sprintf("/%s", name),
+ )
+
+ qMap := map[string]string{}
+
+ if repoURL != "" {
+ qMap["repository_url"] = repoURL
+ }
+ qs := packageurl.QualifiersFromMap(qMap)
+ identifiers = append(identifiers, packageurl.NewPackageURL(
+ "oci", "", name, shaString, qs, "",
+ ).String())
+
+ // Add a hash to the identifier list in case people want to vex
+ // using the value of the image digest
+ identifiers = append(identifiers, strings.TrimPrefix(shaString, "sha256:"))
+ }
+ return identifiers
+}
+
+// subcomponentIdentifiersFromMatch returns the list of identifiers from the
+// package where grype did the match.
+func subcomponentIdentifiersFromMatch(m *match.Match) []string {
+ ret := []string{}
+ if m.Package.PURL != "" {
+ ret = append(ret, m.Package.PURL)
+ }
+
+ // TODO(puerco):Implement CPE matching in openvex/go-vex
+ /*
+ for _, c := range m.Package.CPEs {
+ ret = append(ret, c.String())
+ }
+ */
+ return ret
+}
+
+// FilterMatches takes a set of scanning results and moves any results marked in
+// the VEX data as fixed or not_affected to the ignored list.
+func (ovm *Processor) FilterMatches(
+ docRaw interface{}, ignoreRules []match.IgnoreRule, pkgContext *pkg.Context, matches *match.Matches, ignoredMatches []match.IgnoredMatch,
+) (*match.Matches, []match.IgnoredMatch, error) {
+ doc, ok := docRaw.(*openvex.VEX)
+ if !ok {
+ return nil, nil, errors.New("unable to cast vex document as openvex")
+ }
+
+ remainingMatches := match.NewMatches()
+
+ products, err := productIdentifiersFromContext(pkgContext)
+ if err != nil {
+ return nil, nil, fmt.Errorf("reading product identifiers from context: %w", err)
+ }
+
+ // TODO(alex): should we apply the vex ignore rules to the already ignored matches?
+ // that way the end user sees all of the reasons a match was ignored in case multiple apply
+
+ // Now, let's go through grype's matches
+ sorted := matches.Sorted()
+ for i := range sorted {
+ var statement *openvex.Statement
+ subcmp := subcomponentIdentifiersFromMatch(&sorted[i])
+
+ // Range through the product's different names
+ for _, product := range products {
+ if matchingStatements := doc.Matches(sorted[i].Vulnerability.ID, product, subcmp); len(matchingStatements) != 0 {
+ statement = &matchingStatements[0]
+ break
+ }
+ }
+
+ // No data about this match's component. Next.
+ if statement == nil {
+ remainingMatches.Add(sorted[i])
+ continue
+ }
+
+ rule := matchingRule(ignoreRules, sorted[i], statement, ignoreStatuses)
+ if rule == nil {
+ remainingMatches.Add(sorted[i])
+ continue
+ }
+
+ // Filtering only applies to not_affected and fixed statuses
+ if statement.Status != openvex.StatusNotAffected && statement.Status != openvex.StatusFixed {
+ remainingMatches.Add(sorted[i])
+ continue
+ }
+
+ ignoredMatches = append(ignoredMatches, match.IgnoredMatch{
+ Match: sorted[i],
+ AppliedIgnoreRules: []match.IgnoreRule{*rule},
+ })
+ }
+ return &remainingMatches, ignoredMatches, nil
+}
+
+// matchingRule cycles through a set of ignore rules and returns the first
+// one that matches the statement and the match. Returns nil if none match.
+func matchingRule(ignoreRules []match.IgnoreRule, m match.Match, statement *openvex.Statement, allowedStatuses []openvex.Status) *match.IgnoreRule {
+ ms := match.NewMatches()
+ ms.Add(m)
+
+ revStatuses := map[string]struct{}{}
+ for _, s := range allowedStatuses {
+ revStatuses[string(s)] = struct{}{}
+ }
+
+ for _, rule := range ignoreRules {
+ // If the rule has more conditions than just the VEX statement, check if
+ // it applies to the current match.
+ if rule.HasConditions() {
+ r := rule
+ r.VexStatus = ""
+ if _, ignored := match.ApplyIgnoreRules(ms, []match.IgnoreRule{r}); len(ignored) == 0 {
+ continue
+ }
+ }
+
+ // If the status in the statement is not the same in the rule
+ // and the vex statement, it does not apply
+ if string(statement.Status) != rule.VexStatus {
+ continue
+ }
+
+ // If the rule has a statement other than the allowed ones, skip:
+ if len(revStatuses) > 0 && rule.VexStatus != "" {
+ if _, ok := revStatuses[rule.VexStatus]; !ok {
+ continue
+ }
+ }
+
+ // If the rule applies to a VEX justification it needs to match the
+ // statement, note that justifications only apply to not_affected:
+ if statement.Status == openvex.StatusNotAffected && rule.VexJustification != "" &&
+ rule.VexJustification != string(statement.Justification) {
+ continue
+ }
+
+ // If the vulnerability is blank in the rule it means we will honor
+ // any status with any vulnerability.
+ if rule.Vulnerability == "" {
+ return &rule
+ }
+
+ // If the vulnerability is set, the rule applies if it is the same
+ // in the statement and the rule.
+ if statement.Vulnerability.Matches(rule.Vulnerability) {
+ return &rule
+ }
+ }
+ return nil
+}
+
+// AugmentMatches adds results to the match.Matches array when matching data
+// about an affected VEX product is found on loaded VEX documents. Matches
+// are moved from the ignore list or synthesized when no previous data is found.
+func (ovm *Processor) AugmentMatches(
+ docRaw interface{}, ignoreRules []match.IgnoreRule, pkgContext *pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch,
+) (*match.Matches, []match.IgnoredMatch, error) {
+ doc, ok := docRaw.(*openvex.VEX)
+ if !ok {
+ return nil, nil, errors.New("unable to cast vex document as openvex")
+ }
+
+ additionalIgnoredMatches := []match.IgnoredMatch{}
+
+ products, err := productIdentifiersFromContext(pkgContext)
+ if err != nil {
+ return nil, nil, fmt.Errorf("reading product identifiers from context: %w", err)
+ }
+
+ // Now, let's go through grype's matches
+ for i := range ignoredMatches {
+ var statement *openvex.Statement
+ var searchedBy *SearchedBy
+ subcmp := subcomponentIdentifiersFromMatch(&ignoredMatches[i].Match)
+
+ // Range through the product's different names to see if they match the
+ // statement data
+ for _, product := range products {
+ if matchingStatements := doc.Matches(ignoredMatches[i].Vulnerability.ID, product, subcmp); len(matchingStatements) != 0 {
+ if matchingStatements[0].Status != openvex.StatusAffected &&
+ matchingStatements[0].Status != openvex.StatusUnderInvestigation {
+ break
+ }
+ statement = &matchingStatements[0]
+ searchedBy = &SearchedBy{
+ Vulnerability: ignoredMatches[i].Vulnerability.ID,
+ Product: product,
+ Subcomponents: subcmp,
+ }
+ break
+ }
+ }
+
+ // No data about this match's component. Next.
+ if statement == nil {
+ additionalIgnoredMatches = append(additionalIgnoredMatches, ignoredMatches[i])
+ continue
+ }
+
+ // Only match if rules to augment are configured
+ rule := matchingRule(ignoreRules, ignoredMatches[i].Match, statement, augmentStatuses)
+ if rule == nil {
+ additionalIgnoredMatches = append(additionalIgnoredMatches, ignoredMatches[i])
+ continue
+ }
+
+ newMatch := ignoredMatches[i].Match
+ newMatch.Details = append(newMatch.Details, match.Detail{
+ Type: match.ExactDirectMatch,
+ SearchedBy: searchedBy,
+ Found: Match{
+ Statement: *statement,
+ },
+ Matcher: match.OpenVexMatcher,
+ })
+
+ remainingMatches.Add(newMatch)
+ }
+
+ return remainingMatches, additionalIgnoredMatches, nil
+}
diff --git a/grype/vex/openvex/implementation_test.go b/grype/vex/openvex/implementation_test.go
new file mode 100644
index 00000000000..47c36b12c22
--- /dev/null
+++ b/grype/vex/openvex/implementation_test.go
@@ -0,0 +1,60 @@
+package openvex
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestIdentifiersFromTags(t *testing.T) {
+ for _, tc := range []struct {
+ sut string
+ name string
+ expected []string
+ }{
+ {
+ "alpine:v1.2.3",
+ "alpine",
+ []string{"alpine:v1.2.3", "pkg:oci/alpine?tag=v1.2.3"},
+ },
+ {
+ "alpine",
+ "alpine",
+ []string{"alpine"},
+ },
+ } {
+ res := identifiersFromTags([]string{tc.sut}, tc.name)
+ require.Equal(t, tc.expected, res)
+ }
+}
+
+func TestIdentifiersFromDigests(t *testing.T) {
+ for _, tc := range []struct {
+ sut string
+ expected []string
+ }{
+ {
+ "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ []string{
+ "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126?repository_url=index.docker.io%2Flibrary",
+ "124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ },
+ },
+ {
+ "cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc",
+ []string{
+ "cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc",
+ "pkg:oci/curl@sha256%3A9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc?repository_url=cgr.dev%2Fchainguard",
+ "9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc",
+ },
+ },
+ {
+ "alpine",
+ []string{"alpine"},
+ },
+ } {
+ res := identifiersFromDigests([]string{tc.sut})
+ require.Equal(t, tc.expected, res)
+ }
+}
diff --git a/grype/vex/processor.go b/grype/vex/processor.go
new file mode 100644
index 00000000000..2c744d9f360
--- /dev/null
+++ b/grype/vex/processor.go
@@ -0,0 +1,112 @@
+package vex
+
+import (
+ "fmt"
+
+ gopenvex "github.com/openvex/go-vex/pkg/vex"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/vex/openvex"
+)
+
+type Status string
+
+const (
+ StatusNotAffected Status = Status(gopenvex.StatusNotAffected)
+ StatusAffected Status = Status(gopenvex.StatusAffected)
+ StatusFixed Status = Status(gopenvex.StatusFixed)
+ StatusUnderInvestigation Status = Status(gopenvex.StatusUnderInvestigation)
+)
+
+type Processor struct {
+ Options ProcessorOptions
+ impl vexProcessorImplementation
+}
+
+type vexProcessorImplementation interface {
+ // ReadVexDocuments takes a list of vex filenames and returns a single
+ // value representing the VEX information in the underlying implementation's
+ // format. Returns an error if the files cannot be processed.
+ ReadVexDocuments(docs []string) (interface{}, error)
+
+ // FilterMatches matches receives the underlying VEX implementation VEX data and
+ // the scanning context and matching results and filters the fixed and
+ // not_affected results,moving them to the list of ignored matches.
+ FilterMatches(interface{}, []match.IgnoreRule, *pkg.Context, *match.Matches, []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error)
+
+ // AugmentMatches reads known affected VEX products from loaded documents and
+ // adds new results to the scanner results when the product is marked as
+ // affected in the VEX data.
+ AugmentMatches(interface{}, []match.IgnoreRule, *pkg.Context, *match.Matches, []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error)
+}
+
+// getVexImplementation this function returns the vex processor implementation
+// at some point it can read the options and choose a user configured implementation.
+func getVexImplementation() vexProcessorImplementation {
+ return openvex.New()
+}
+
+// NewProcessor returns a new VEX processor. For now, it defaults to the only vex
+// implementation: OpenVEX
+func NewProcessor(opts ProcessorOptions) *Processor {
+ return &Processor{
+ Options: opts,
+ impl: getVexImplementation(),
+ }
+}
+
+// ProcessorOptions captures the optiones of the VEX processor.
+type ProcessorOptions struct {
+ Documents []string
+ IgnoreRules []match.IgnoreRule
+}
+
+// ApplyVEX receives the results from a scan run and applies any VEX information
+// in the files specified in the grype invocation. Any filtered results will
+// be moved to the ignored matches slice.
+func (vm *Processor) ApplyVEX(pkgContext *pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error) {
+ var err error
+
+ // If no VEX documents are loaded, just pass through the matches, effectivle NOOP
+ if len(vm.Options.Documents) == 0 {
+ return remainingMatches, ignoredMatches, nil
+ }
+
+ // Read VEX data from all passed documents
+ rawVexData, err := vm.impl.ReadVexDocuments(vm.Options.Documents)
+ if err != nil {
+ return nil, nil, fmt.Errorf("parsing vex document: %w", err)
+ }
+
+ vexRules := extractVexRules(vm.Options.IgnoreRules)
+
+ remainingMatches, ignoredMatches, err = vm.impl.FilterMatches(
+ rawVexData, vexRules, pkgContext, remainingMatches, ignoredMatches,
+ )
+ if err != nil {
+ return nil, nil, fmt.Errorf("checking matches against VEX data: %w", err)
+ }
+
+ remainingMatches, ignoredMatches, err = vm.impl.AugmentMatches(
+ rawVexData, vexRules, pkgContext, remainingMatches, ignoredMatches,
+ )
+ if err != nil {
+ return nil, nil, fmt.Errorf("checking matches to augment from VEX data: %w", err)
+ }
+
+ return remainingMatches, ignoredMatches, nil
+}
+
+// extractVexRules is a utility function that takes a set of ignore rules and
+// extracts those that act on VEX statuses.
+func extractVexRules(rules []match.IgnoreRule) []match.IgnoreRule {
+ newRules := []match.IgnoreRule{}
+ for _, r := range rules {
+ if r.VexStatus != "" {
+ newRules = append(newRules, r)
+ newRules[len(newRules)-1].Namespace = "vex"
+ }
+ }
+ return newRules
+}
diff --git a/grype/vex/processor_test.go b/grype/vex/processor_test.go
new file mode 100644
index 00000000000..86a7d0d128f
--- /dev/null
+++ b/grype/vex/processor_test.go
@@ -0,0 +1,319 @@
+package vex
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/grype/grype/match"
+ "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/syft/syft/source"
+)
+
+func TestProcessor_ApplyVEX(t *testing.T) {
+ pkgContext := &pkg.Context{
+ Source: &source.Description{
+ Name: "alpine",
+ Version: "3.17",
+ Metadata: source.ImageMetadata{
+ RepoDigests: []string{
+ "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ },
+ },
+ },
+ Distro: nil,
+ }
+
+ libCryptoPackage := pkg.Package{
+ ID: "cc8f90662d91481d",
+ Name: "libcrypto3",
+ Version: "3.0.8-r3",
+
+ Type: "apk",
+ PURL: "pkg:apk/alpine/libcrypto3@3.0.8-r3?arch=x86_64&upstream=openssl&distro=alpine-3.17.3",
+ Upstreams: []pkg.UpstreamPackage{
+ {
+ Name: "openssl",
+ },
+ },
+ }
+
+ libCryptoCVE_2023_3817 := match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-3817",
+ Namespace: "alpine:distro:alpine:3.17",
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"3.0.10-r0"},
+ State: vulnerability.FixStateFixed,
+ },
+ },
+ Package: libCryptoPackage,
+ }
+
+ libCryptoCVE_2023_1255 := match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-1255",
+ Namespace: "alpine:distro:alpine:3.17",
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"3.0.8-r4"},
+ State: vulnerability.FixStateFixed,
+ },
+ },
+ Package: libCryptoPackage,
+ }
+
+ libCryptoCVE_2023_2975 := match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-2023-2975",
+ Namespace: "alpine:distro:alpine:3.17",
+ },
+ Fix: vulnerability.Fix{
+ Versions: []string{"3.0.9-r2"},
+ State: vulnerability.FixStateFixed,
+ },
+ },
+ Package: libCryptoPackage,
+ }
+
+ getSubject := func() *match.Matches {
+ s := match.NewMatches(
+ // not-affected justification example
+ libCryptoCVE_2023_3817,
+
+ // fixed status example + matching CVE
+ libCryptoCVE_2023_1255,
+
+ // fixed status example
+ libCryptoCVE_2023_2975,
+ )
+
+ return &s
+ }
+
+ matchesRef := func(ms ...match.Match) *match.Matches {
+ m := match.NewMatches(ms...)
+ return &m
+ }
+
+ type args struct {
+ pkgContext *pkg.Context
+ matches *match.Matches
+ ignoredMatches []match.IgnoredMatch
+ }
+
+ tests := []struct {
+ name string
+ options ProcessorOptions
+ args args
+ wantMatches *match.Matches
+ wantIgnoredMatches []match.IgnoredMatch
+ wantErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "openvex-demo1 - ignore by fixed status",
+ options: ProcessorOptions{
+ Documents: []string{
+ "testdata/vex-docs/openvex-demo1.json",
+ },
+ IgnoreRules: []match.IgnoreRule{
+ {
+ VexStatus: "fixed",
+ },
+ },
+ },
+ args: args{
+ pkgContext: pkgContext,
+ matches: getSubject(),
+ },
+ wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975),
+ wantIgnoredMatches: []match.IgnoredMatch{
+ {
+ Match: libCryptoCVE_2023_1255,
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Namespace: "vex", // note: an additional namespace was added
+ VexStatus: "fixed",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "openvex-demo1 - ignore by fixed status and CVE", // no real difference from the first test other than the AppliedIgnoreRules
+ options: ProcessorOptions{
+ Documents: []string{
+ "testdata/vex-docs/openvex-demo1.json",
+ },
+ IgnoreRules: []match.IgnoreRule{
+ {
+ Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test
+ VexStatus: "fixed",
+ },
+ },
+ },
+ args: args{
+ pkgContext: pkgContext,
+ matches: getSubject(),
+ },
+ wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975),
+ wantIgnoredMatches: []match.IgnoredMatch{
+ {
+ Match: libCryptoCVE_2023_1255,
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Namespace: "vex",
+ Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test
+ VexStatus: "fixed",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "openvex-demo2 - ignore by fixed status",
+ options: ProcessorOptions{
+ Documents: []string{
+ "testdata/vex-docs/openvex-demo2.json",
+ },
+ IgnoreRules: []match.IgnoreRule{
+ {
+ VexStatus: "fixed",
+ },
+ },
+ },
+ args: args{
+ pkgContext: pkgContext,
+ matches: getSubject(),
+ },
+ wantMatches: matchesRef(libCryptoCVE_2023_3817),
+ wantIgnoredMatches: []match.IgnoredMatch{
+ {
+ Match: libCryptoCVE_2023_1255,
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Namespace: "vex",
+ VexStatus: "fixed",
+ },
+ },
+ },
+ {
+ Match: libCryptoCVE_2023_2975,
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Namespace: "vex",
+ VexStatus: "fixed",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "openvex-demo2 - ignore by fixed status and CVE",
+ options: ProcessorOptions{
+ Documents: []string{
+ "testdata/vex-docs/openvex-demo2.json",
+ },
+ IgnoreRules: []match.IgnoreRule{
+ {
+ Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test
+ VexStatus: "fixed",
+ },
+ },
+ },
+ args: args{
+ pkgContext: pkgContext,
+ matches: getSubject(),
+ },
+ wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975),
+ wantIgnoredMatches: []match.IgnoredMatch{
+ {
+ Match: libCryptoCVE_2023_1255,
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Namespace: "vex",
+ Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test
+ VexStatus: "fixed",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "openvex-demo1 - ignore by not_affected status and vulnerable_code_not_present justification",
+ options: ProcessorOptions{
+ Documents: []string{
+ "testdata/vex-docs/openvex-demo1.json",
+ },
+ IgnoreRules: []match.IgnoreRule{
+ {
+ VexStatus: "not_affected",
+ VexJustification: "vulnerable_code_not_present",
+ },
+ },
+ },
+ args: args{
+ pkgContext: pkgContext,
+ matches: getSubject(),
+ },
+ // nothing gets ignored!
+ wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975, libCryptoCVE_2023_1255),
+ wantIgnoredMatches: []match.IgnoredMatch{},
+ },
+ {
+ name: "openvex-demo2 - ignore by not_affected status and vulnerable_code_not_present justification",
+ options: ProcessorOptions{
+ Documents: []string{
+ "testdata/vex-docs/openvex-demo2.json",
+ },
+ IgnoreRules: []match.IgnoreRule{
+ {
+ VexStatus: "not_affected",
+ VexJustification: "vulnerable_code_not_present",
+ },
+ },
+ },
+ args: args{
+ pkgContext: pkgContext,
+ matches: getSubject(),
+ },
+ wantMatches: matchesRef(libCryptoCVE_2023_2975, libCryptoCVE_2023_1255),
+ wantIgnoredMatches: []match.IgnoredMatch{
+ {
+ Match: libCryptoCVE_2023_3817,
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Namespace: "vex",
+ VexStatus: "not_affected",
+ VexJustification: "vulnerable_code_not_present",
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.wantErr == nil {
+ tt.wantErr = require.NoError
+ }
+
+ p := NewProcessor(tt.options)
+ actualMatches, actualIgnoredMatches, err := p.ApplyVEX(tt.args.pkgContext, tt.args.matches, tt.args.ignoredMatches)
+ tt.wantErr(t, err)
+ if err != nil {
+ return
+ }
+
+ assert.Equal(t, tt.wantMatches.Sorted(), actualMatches.Sorted())
+ assert.Equal(t, tt.wantIgnoredMatches, actualIgnoredMatches)
+
+ })
+ }
+}
diff --git a/grype/vex/testdata/vex-docs/openvex-debian.json b/grype/vex/testdata/vex-docs/openvex-debian.json
new file mode 100644
index 00000000000..b1f8cb6af9b
--- /dev/null
+++ b/grype/vex/testdata/vex-docs/openvex-debian.json
@@ -0,0 +1,20 @@
+{
+ "@context": "https://openvex.dev/ns/v0.2.0",
+ "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
+ "author": "The OpenVEX Project ",
+ "timestamp": "2023-07-17T18:28:47.696004345-06:00",
+ "version": 1,
+ "statements": [
+ {
+ "vulnerability": {
+ "name": "CVE-2014-fake-1"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/debian@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126?repository_url=index.docker.io/library"
+ }
+ ],
+ "status": "fixed"
+ }
+ ]
+ }
diff --git a/grype/vex/testdata/vex-docs/openvex-demo1.json b/grype/vex/testdata/vex-docs/openvex-demo1.json
new file mode 100644
index 00000000000..47549499ad5
--- /dev/null
+++ b/grype/vex/testdata/vex-docs/openvex-demo1.json
@@ -0,0 +1,24 @@
+{
+ "@context": "https://openvex.dev/ns/v0.2.0",
+ "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
+ "author": "The OpenVEX Project ",
+ "timestamp": "2023-07-17T18:28:47.696004345-06:00",
+ "version": 1,
+ "statements": [
+ {
+ "vulnerability": {
+ "name": "CVE-2023-1255"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ "subcomponents": [
+ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" },
+ { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" }
+ ]
+ }
+ ],
+ "status": "fixed"
+ }
+ ]
+}
diff --git a/grype/vex/testdata/vex-docs/openvex-demo2.json b/grype/vex/testdata/vex-docs/openvex-demo2.json
new file mode 100644
index 00000000000..637d0907822
--- /dev/null
+++ b/grype/vex/testdata/vex-docs/openvex-demo2.json
@@ -0,0 +1,89 @@
+{
+ "@context": "https://openvex.dev/ns/v0.2.0",
+ "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
+ "author": "The OpenVEX Project ",
+ "role": "Demo Writer",
+ "timestamp": "2023-07-17T18:28:47.696004345-06:00",
+ "version": 1,
+ "statements": [
+ {
+ "vulnerability": {
+ "name": "CVE-2023-1255"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ "subcomponents": [
+ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" },
+ { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" }
+ ]
+ }
+ ],
+ "status": "fixed"
+ },
+ {
+ "vulnerability": {
+ "name": "CVE-2023-2650"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ "subcomponents": [
+ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" },
+ { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" }
+ ]
+ }
+ ],
+ "status": "fixed"
+ },
+ {
+ "vulnerability": {
+ "name": "CVE-2023-2975"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ "subcomponents": [
+ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" },
+ { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" }
+ ]
+ }
+ ],
+ "status": "fixed"
+ },
+ {
+ "vulnerability": {
+ "name": "CVE-2023-3446"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ "subcomponents": [
+ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" },
+ { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" }
+ ]
+ }
+ ],
+ "status": "not_affected",
+ "justification": "vulnerable_code_not_present",
+ "impact_statement": "affected functions were removed before packaging"
+ },
+ {
+ "vulnerability": {
+ "name": "CVE-2023-3817"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ "subcomponents": [
+ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" },
+ { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" }
+ ]
+ }
+ ],
+ "status": "not_affected",
+ "justification": "vulnerable_code_not_present",
+ "impact_statement": "affected functions were removed before packaging"
+ }
+ ]
+}
diff --git a/grype/vulnerability/fix.go b/grype/vulnerability/fix.go
index a8d88a52cf4..fb44b0769c6 100644
--- a/grype/vulnerability/fix.go
+++ b/grype/vulnerability/fix.go
@@ -1,10 +1,28 @@
package vulnerability
-import (
- grypeDb "github.com/anchore/grype/grype/db/v5"
+type FixState string
+
+const (
+ FixStateUnknown FixState = "unknown"
+ FixStateFixed FixState = "fixed"
+ FixStateNotFixed FixState = "not-fixed"
+ FixStateWontFix FixState = "wont-fix"
)
+func AllFixStates() []FixState {
+ return []FixState{
+ FixStateFixed,
+ FixStateNotFixed,
+ FixStateUnknown,
+ FixStateWontFix,
+ }
+}
+
type Fix struct {
Versions []string
- State grypeDb.FixState
+ State FixState
+}
+
+func (f FixState) String() string {
+ return string(f)
}
diff --git a/grype/vulnerability/metadata.go b/grype/vulnerability/metadata.go
index fb87aedb2f2..5351f2baa01 100644
--- a/grype/vulnerability/metadata.go
+++ b/grype/vulnerability/metadata.go
@@ -1,17 +1,125 @@
package vulnerability
import (
- grypeDB "github.com/anchore/grype/grype/db/v5"
+ "strings"
+ "time"
)
type Metadata struct {
- ID string
- DataSource string
- Namespace string
- Severity string
- URLs []string
- Description string
- Cvss []Cvss
+ ID string
+ DataSource string // the primary reference URL, i.e. where the data originated
+ Namespace string
+ Severity 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 {
@@ -29,45 +137,22 @@ type CvssMetrics struct {
ImpactScore *float64
}
-func NewMetadata(m *grypeDB.VulnerabilityMetadata) (*Metadata, error) {
- if m == nil {
- return nil, nil
- }
- return &Metadata{
- ID: m.ID,
- DataSource: m.DataSource,
- Namespace: m.Namespace,
- Severity: m.Severity,
- URLs: m.URLs,
- Description: m.Description,
- Cvss: NewCvss(m.Cvss),
- }, nil
-}
-
-func NewCvss(m []grypeDB.Cvss) []Cvss {
- //nolint:prealloc
- var cvss []Cvss
- for _, score := range m {
- cvss = append(cvss, Cvss{
- Source: score.Source,
- Type: score.Type,
- Version: score.Version,
- Vector: score.Vector,
- Metrics: CvssMetrics{
- BaseScore: score.Metrics.BaseScore,
- ExploitabilityScore: score.Metrics.ExploitabilityScore,
- ImpactScore: score.Metrics.ImpactScore,
- },
- VendorMetadata: score.VendorMetadata,
- })
- }
- return cvss
+type KnownExploited struct {
+ CVE string
+ VendorProject string
+ Product string
+ DateAdded *time.Time
+ RequiredAction string
+ DueDate *time.Time
+ KnownRansomwareCampaignUse string
+ Notes string
+ URLs []string
+ CWEs []string
}
-func NewCvssMetrics(baseScore, exploitabilityScore, impactScore float64) CvssMetrics {
- return CvssMetrics{
- BaseScore: baseScore,
- ExploitabilityScore: &exploitabilityScore,
- ImpactScore: &impactScore,
- }
+type EPSS struct {
+ CVE string
+ EPSS float64
+ Percentile float64
+ Date time.Time
}
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
new file mode 100644
index 00000000000..276daa95798
--- /dev/null
+++ b/grype/vulnerability/mock/vulnerability_provider.go
@@ -0,0 +1,84 @@
+package mock
+
+import (
+ "github.com/anchore/grype/grype/db/v6/name"
+ grypePkg "github.com/anchore/grype/grype/pkg"
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/vulnerability"
+)
+
+// VulnerabilityProvider returns a new mock implementation of a vulnerability Provider, with the provided set of vulnerabilities
+func VulnerabilityProvider(vulnerabilities ...vulnerability.Vulnerability) vulnerability.Provider {
+ return &mockProvider{
+ Vulnerabilities: vulnerabilities,
+ }
+}
+
+type mockProvider struct {
+ Vulnerabilities []vulnerability.Vulnerability
+}
+
+func (s *mockProvider) Close() error {
+ return nil
+}
+
+func (s *mockProvider) PackageSearchNames(p grypePkg.Package) []string {
+ return name.PackageNames(p)
+}
+
+// VulnerabilityMetadata returns the metadata associated with a vulnerability
+func (s *mockProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) {
+ for _, vuln := range s.Vulnerabilities {
+ if vuln.ID == ref.ID && vuln.Namespace == ref.Namespace {
+ var meta *vulnerability.Metadata
+ if m, ok := vuln.Internal.(vulnerability.Metadata); ok {
+ meta = &m
+ }
+ if m, ok := vuln.Internal.(*vulnerability.Metadata); ok {
+ meta = m
+ }
+ if meta != nil {
+ if meta.ID != vuln.ID {
+ meta.ID = vuln.ID
+ }
+ if meta.Namespace != vuln.Namespace {
+ meta.Namespace = vuln.Namespace
+ }
+ return meta, nil
+ }
+ }
+ }
+ return nil, nil
+}
+
+func (s *mockProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) {
+ if err := search.ValidateCriteria(criteria); err != nil {
+ return nil, err
+ }
+
+ var out []vulnerability.Vulnerability
+ out = append(out, s.Vulnerabilities...)
+ return filterE(out, func(v vulnerability.Vulnerability) (bool, error) {
+ for _, c := range criteria {
+ matches, _, err := c.MatchesVulnerability(v)
+ if !matches || err != nil {
+ return false, err
+ }
+ }
+ return true, nil
+ })
+}
+
+func filterE[T any](out []T, keep func(v T) (bool, error)) ([]T, error) {
+ for i := 0; i < len(out); i++ {
+ ok, err := keep(out[i])
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ out = append(out[:i], out[i+1:]...)
+ i--
+ }
+ }
+ return out, nil
+}
diff --git a/grype/vulnerability/provider.go b/grype/vulnerability/provider.go
index eb516e398cc..d395bcecd13 100644
--- a/grype/vulnerability/provider.go
+++ b/grype/vulnerability/provider.go
@@ -1,31 +1,94 @@
package vulnerability
import (
- "github.com/anchore/grype/grype/distro"
- "github.com/anchore/grype/grype/pkg"
- "github.com/anchore/syft/syft/cpe"
- syftPkg "github.com/anchore/syft/syft/pkg"
+ "encoding/json"
+ "io"
+ "time"
+
+ grypePkg "github.com/anchore/grype/grype/pkg"
)
+// Criteria interfaces are used for FindVulnerabilities calls
+type Criteria interface {
+ // MatchesVulnerability returns true if the provided value meets the criteria
+ MatchesVulnerability(value Vulnerability) (bool, string, error)
+}
+
+// MetadataProvider implementations provide ways to look up vulnerability metadata
+// Deprecated: vulnerability.Vulnerability objects now have metadata included
+type MetadataProvider interface {
+ // VulnerabilityMetadata returns the metadata associated with a vulnerability
+ // Deprecated: vulnerability.Vulnerability objects now have metadata included
+ VulnerabilityMetadata(ref Reference) (*Metadata, error)
+}
+
+// Provider is the common interface for vulnerability sources to provide searching and metadata, such as a database
type Provider interface {
- Get(id, namespace string) ([]Vulnerability, error)
- ProviderByDistro
- ProviderByLanguage
- ProviderByCPE
+ PackageSearchNames(grypePkg.Package) []string
+ // FindVulnerabilities returns vulnerabilities matching all the provided criteria
+ FindVulnerabilities(criteria ...Criteria) ([]Vulnerability, error)
+
+ MetadataProvider
+
+ io.Closer
}
-type ProviderByDistro interface {
- GetByDistro(*distro.Distro, pkg.Package) ([]Vulnerability, error)
+type StoreMetadataProvider interface {
+ DataProvenance() (map[string]DataProvenance, error)
}
-type ProviderByLanguage interface {
- GetByLanguage(syftPkg.Language, pkg.Package) ([]Vulnerability, error)
+type DataProvenance struct {
+ DateCaptured time.Time `json:"captured,omitempty"`
+ InputDigest string `json:"input,omitempty"`
}
-type ProviderByCPE interface {
- GetByCPE(cpe.CPE) ([]Vulnerability, error)
+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"`
}
-type MetadataProvider interface {
- GetMetadata(id, namespace string) (*Metadata, error)
+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/schema_version.go b/grype/vulnerability/schema_version.go
deleted file mode 100644
index 7d044f43991..00000000000
--- a/grype/vulnerability/schema_version.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package vulnerability
-
-import grypeDB "github.com/anchore/grype/grype/db/v5"
-
-const SchemaVersion = grypeDB.SchemaVersion
diff --git a/grype/vulnerability/set.go b/grype/vulnerability/set.go
deleted file mode 100644
index 42a5f5addba..00000000000
--- a/grype/vulnerability/set.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package vulnerability
-
-type Set map[string]struct{}
-
-func NewSet() Set {
- return make(Set)
-}
-
-func (s Set) Add(v *Vulnerability) {
- s[v.hash()] = struct{}{}
-}
-
-func (s Set) Remove(v *Vulnerability) {
- delete(s, v.hash())
-}
-
-func (s Set) Contains(v *Vulnerability) bool {
- _, ok := s[v.hash()]
- return ok
-}
diff --git a/grype/vulnerability/severity.go b/grype/vulnerability/severity.go
index aa77c355c2a..7e427e415e0 100644
--- a/grype/vulnerability/severity.go
+++ b/grype/vulnerability/severity.go
@@ -12,7 +12,7 @@ const (
)
var matcherTypeStr = []string{
- "unknown severity",
+ "unknown", // "unknown severity",
"negligible",
"low",
"medium",
diff --git a/grype/vulnerability/vulnerability.go b/grype/vulnerability/vulnerability.go
index 25d9f0fcbec..ad5166a558a 100644
--- a/grype/vulnerability/vulnerability.go
+++ b/grype/vulnerability/vulnerability.go
@@ -3,7 +3,6 @@ package vulnerability
import (
"fmt"
- grypeDB "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/pkg/qualifier"
"github.com/anchore/grype/grype/version"
"github.com/anchore/syft/syft/cpe"
@@ -12,67 +11,26 @@ import (
type Reference struct {
ID string
Namespace string
+ Internal any
}
type Vulnerability struct {
+ Reference
+ Status string
+ PackageName string
Constraint version.Constraint
PackageQualifiers []qualifier.Qualifier
CPEs []cpe.CPE
- ID string
- Namespace string
Fix Fix
Advisories []Advisory
RelatedVulnerabilities []Reference
-}
-
-func NewVulnerability(vuln grypeDB.Vulnerability) (*Vulnerability, error) {
- format := version.ParseFormat(vuln.VersionFormat)
-
- constraint, err := version.GetConstraint(vuln.VersionConstraint, format)
- if err != nil {
- return nil, fmt.Errorf("failed to parse constraint='%s' format='%s': %w", vuln.VersionConstraint, format, err)
- }
-
- pkgQualifiers := make([]qualifier.Qualifier, len(vuln.PackageQualifiers))
- for idx, q := range vuln.PackageQualifiers {
- pkgQualifiers[idx] = q.Parse()
- }
-
- advisories := make([]Advisory, len(vuln.Advisories))
- for idx, advisory := range vuln.Advisories {
- advisories[idx] = Advisory{
- ID: advisory.ID,
- Link: advisory.Link,
- }
- }
-
- var relatedVulnerabilities []Reference
- for _, r := range vuln.RelatedVulnerabilities {
- relatedVulnerabilities = append(relatedVulnerabilities, Reference{
- ID: r.ID,
- Namespace: r.Namespace,
- })
- }
-
- return &Vulnerability{
- Constraint: constraint,
- ID: vuln.ID,
- CPEs: make([]cpe.CPE, 0),
- Namespace: vuln.Namespace,
- PackageQualifiers: pkgQualifiers,
- Fix: Fix{
- Versions: vuln.Fix.Versions,
- State: vuln.Fix.State,
- },
- Advisories: advisories,
- RelatedVulnerabilities: relatedVulnerabilities,
- }, nil
+ Metadata *Metadata
}
func (v Vulnerability) String() string {
- return fmt.Sprintf("Vuln(id=%s constraint=%q qualifiers=%+v)", v.ID, v.Constraint.String(), v.PackageQualifiers)
-}
-
-func (v *Vulnerability) hash() string {
- return fmt.Sprintf("%s|%s|%+v|%+v", v.ID, v.Constraint, v.PackageQualifiers, v.CPEs)
+ constraint := "(none)"
+ if v.Constraint != nil {
+ constraint = v.Constraint.String()
+ }
+ return fmt.Sprintf("Vuln(id=%s constraint=%q qualifiers=%+v)", v.ID, constraint, v.PackageQualifiers)
}
diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go
index 3eff7f81d25..0df6da3ca63 100644
--- a/grype/vulnerability_matcher.go
+++ b/grype/vulnerability_matcher.go
@@ -1,30 +1,41 @@
package grype
import (
+ "errors"
+ "fmt"
+ "runtime/debug"
"strings"
+ "github.com/wagoodman/go-partybus"
+ "github.com/wagoodman/go-progress"
+
+ "github.com/anchore/grype/grype/distro"
+ "github.com/anchore/grype/grype/event"
+ "github.com/anchore/grype/grype/event/monitor"
"github.com/anchore/grype/grype/grypeerr"
"github.com/anchore/grype/grype/match"
- "github.com/anchore/grype/grype/matcher"
+ "github.com/anchore/grype/grype/matcher/stock"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/store"
+ "github.com/anchore/grype/grype/vex"
"github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/internal/bus"
"github.com/anchore/grype/internal/log"
+ syftPkg "github.com/anchore/syft/syft/pkg"
)
-type VulnerabilityMatcher struct {
- Store store.Store
- Matchers []matcher.Matcher
- IgnoreRules []match.IgnoreRule
- FailSeverity *vulnerability.Severity
- NormalizeByCVE bool
-}
+const (
+ branch = "├──"
+ leaf = "└──"
+)
-func DefaultVulnerabilityMatcher(store store.Store) *VulnerabilityMatcher {
- return &VulnerabilityMatcher{
- Store: store,
- Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
- }
+type VulnerabilityMatcher struct {
+ VulnerabilityProvider vulnerability.Provider
+ ExclusionProvider match.ExclusionProvider
+ Matchers []match.Matcher
+ IgnoreRules []match.IgnoreRule
+ FailSeverity *vulnerability.Severity
+ NormalizeByCVE bool
+ VexProcessor *vex.Processor
}
func (m *VulnerabilityMatcher) FailAtOrAboveSeverity(severity *vulnerability.Severity) *VulnerabilityMatcher {
@@ -32,7 +43,7 @@ func (m *VulnerabilityMatcher) FailAtOrAboveSeverity(severity *vulnerability.Sev
return m
}
-func (m *VulnerabilityMatcher) WithMatchers(matchers []matcher.Matcher) *VulnerabilityMatcher {
+func (m *VulnerabilityMatcher) WithMatchers(matchers []match.Matcher) *VulnerabilityMatcher {
m.Matchers = matchers
return m
}
@@ -42,9 +53,55 @@ func (m *VulnerabilityMatcher) WithIgnoreRules(ignoreRules []match.IgnoreRule) *
return m
}
-func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Context) (*match.Matches, []match.IgnoredMatch, error) {
+func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Context) (remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, err error) {
+ progressMonitor := trackMatcher(len(pkgs))
+
+ defer func() {
+ progressMonitor.Ignored.Set(int64(len(ignoredMatches)))
+ progressMonitor.SetCompleted()
+ if err != nil {
+ progressMonitor.MatchesDiscovered.SetError(err)
+ }
+ }()
+
+ 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 {
+ err = fmt.Errorf("unable to find matches against VEX sources: %w", err)
+ return remainingMatches, ignoredMatches, err
+ }
+
+ if m.FailSeverity != nil && hasSeverityAtOrAbove(m.VulnerabilityProvider, *m.FailSeverity, *remainingMatches) {
+ err = grypeerr.ErrAboveSeverityThreshold
+ return remainingMatches, ignoredMatches, err
+ }
+
+ logListSummary(progressMonitor)
+
+ logIgnoredMatches(ignoredMatches)
+
+ return remainingMatches, ignoredMatches, nil
+}
+
+func (m *VulnerabilityMatcher) findDBMatches(pkgs []pkg.Package, context pkg.Context, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) {
var ignoredMatches []match.IgnoredMatch
- matches := matcher.FindMatches(m.Store, context.Distro, m.Matchers, pkgs)
+
+ log.Trace("finding matches against DB")
+ matches, err := m.searchDBForMatches(context.Distro, pkgs, progressMonitor)
+ if err != nil {
+ 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")
+ }
matches, ignoredMatches = m.applyIgnoreRules(matches)
@@ -58,17 +115,133 @@ func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Conte
// regresses the results (relative to the already applied ignore rules). Why do we additionally apply
// the ignore rules before normalizing? In case the user has a rule that ignores a non-normalized
// vulnerability ID, we wantMatches to ensure that the rule is honored.
+ originalIgnoredMatches := ignoredMatches
matches, ignoredMatches = m.applyIgnoreRules(normalizedMatches)
+ ignoredMatches = m.mergeIgnoredMatches(originalIgnoredMatches, ignoredMatches)
}
- var err error
- if m.FailSeverity != nil && HasSeverityAtOrAbove(m.Store, *m.FailSeverity, matches) {
- err = grypeerr.ErrAboveSeverityThreshold
+ return &matches, ignoredMatches, nil
+}
+
+func (m *VulnerabilityMatcher) mergeIgnoredMatches(allIgnoredMatches ...[]match.IgnoredMatch) []match.IgnoredMatch {
+ var out []match.IgnoredMatch
+ for _, ignoredMatches := range allIgnoredMatches {
+ for _, ignored := range ignoredMatches {
+ if m.NormalizeByCVE {
+ ignored.Match = m.normalizeByCVE(ignored.Match)
+ }
+ out = append(out, ignored)
+ }
+ }
+ return out
+}
+
+//nolint:funlen
+func (m *VulnerabilityMatcher) searchDBForMatches(
+ d *distro.Distro,
+ packages []pkg.Package,
+ progressMonitor *monitorWriter,
+) (match.Matches, error) {
+ var allMatches []match.Match
+ var allIgnored []match.IgnoredMatch
+ matcherIndex, defaultMatcher := newMatcherIndex(m.Matchers)
+
+ 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")
+
+ // if there is no distro set, default to global distro
+ orig := p.Distro
+ if orig == nil {
+ p.Distro = d
+ }
+
+ matchAgainst, ok := matcherIndex[p.Type]
+ if !ok {
+ matchAgainst = []match.Matcher{defaultMatcher}
+ }
+ for _, theMatcher := range matchAgainst {
+ 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")
+ matcherErrs = append(matcherErrs, err)
+ }
+
+ allIgnored = append(allIgnored, ignoredMatches...)
+
+ // Filter out matches based on records in the database exclusion table and hard-coded rules
+ filtered, dropped := match.ApplyExplicitIgnoreRules(m.ExclusionProvider, match.NewMatches(matches...))
+
+ additionalMatches := filtered.Sorted()
+ logPackageMatches(p, additionalMatches)
+ logExplicitDroppedPackageMatches(p, dropped)
+ allMatches = append(allMatches, additionalMatches...)
+
+ progressMonitor.MatchesDiscovered.Add(int64(len(additionalMatches)))
+
+ // note: there is a difference between "ignore" and "dropped" matches.
+ // ignored: matches that are filtered out due to user-provided ignore rules
+ // dropped: matches that are filtered out due to hard-coded rules
+ updateVulnerabilityList(progressMonitor, additionalMatches, nil, dropped, m.VulnerabilityProvider)
+ }
+
+ p.Distro = orig
+ }
+
+ // apply ignores based on matchers returning ignore rules
+ filtered, dropped := match.ApplyIgnoreFilters(allMatches, ignoredMatchFilter(allIgnored))
+ logIgnoredMatches(dropped)
+
+ // get deduplicated set of matches
+ res := match.NewMatches(filtered...)
+
+ // update the total discovered matches after removing all duplicates and ignores
+ progressMonitor.MatchesDiscovered.Set(int64(res.Count()))
+
+ 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) {
+ if m.VexProcessor == nil {
+ log.Trace("no VEX documents provided, skipping VEX matching")
+ return remainingMatches, ignoredMatches, nil
+ }
+
+ log.Trace("finding matches against available VEX documents")
+ matchesAfterVex, ignoredMatchesAfterVex, err := m.VexProcessor.ApplyVEX(&context, remainingMatches, ignoredMatches)
+ if err != nil {
+ return nil, nil, fmt.Errorf("unable to find matches against VEX documents: %w", err)
}
- return &matches, ignoredMatches, err
+ diffMatches := matchesAfterVex.Diff(*remainingMatches)
+ // note: this assumes that the diff can only be additive
+ diffIgnoredMatches := ignoredMatchesDiff(ignoredMatchesAfterVex, ignoredMatches)
+
+ updateVulnerabilityList(progressMonitor, diffMatches.Sorted(), diffIgnoredMatches, nil, m.VulnerabilityProvider)
+
+ return matchesAfterVex, ignoredMatchesAfterVex, nil
}
+// applyIgnoreRules applies the user-provided ignore rules, splitting ignored matches into a separate set
func (m *VulnerabilityMatcher) applyIgnoreRules(matches match.Matches) (match.Matches, []match.IgnoredMatch) {
var ignoredMatches []match.IgnoredMatch
if len(m.IgnoreRules) == 0 {
@@ -98,20 +271,27 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match {
switch len(effectiveCVERecordRefs) {
case 0:
- // TODO: trace logging
+ log.WithFields(
+ "vuln", match.Vulnerability.ID,
+ "package", displayPackage(match.Package),
+ ).Trace("unable to find CVE record for vulnerability, skipping normalization")
return match
case 1:
break
default:
- // TODO: trace logging
+ log.WithFields(
+ "refs", fmt.Sprintf("%+v", effectiveCVERecordRefs),
+ "vuln", match.Vulnerability.ID,
+ "package", displayPackage(match.Package),
+ ).Trace("found multiple CVE records for vulnerability, skipping normalization")
return match
}
ref := effectiveCVERecordRefs[0]
- upstreamMetadata, err := m.Store.GetMetadata(ref.ID, ref.Namespace)
+ upstreamMetadata, err := m.VulnerabilityProvider.VulnerabilityMetadata(ref)
if err != nil {
- log.Warnf("unable to fetch effective CVE metadata for id=%q namespace=%q : %v", ref.ID, ref.Namespace, err)
+ log.WithFields("id", ref.ID, "namespace", ref.Namespace, "error", err).Warn("unable to fetch effective CVE metadata")
return match
}
@@ -131,16 +311,105 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match {
return match
}
+// ignoreRulesByLocation implements match.IgnoreFilter to filter each matching
+// package that overlaps by location and have the same vulnerability ID (CVE)
+type ignoreRulesByLocation struct {
+ locationToIgnoreRules map[string][]match.IgnoreRule
+}
+
+func (i ignoreRulesByLocation) IgnoreMatch(m match.Match) []match.IgnoreRule {
+ for _, l := range m.Package.Locations.ToSlice() {
+ for _, rule := range i.locationToIgnoreRules[l.RealPath] {
+ if rule.Vulnerability == m.Vulnerability.ID {
+ return []match.IgnoreRule{rule}
+ }
+ for _, relatedVulnerability := range m.Vulnerability.RelatedVulnerabilities {
+ if rule.Vulnerability == relatedVulnerability.ID {
+ return []match.IgnoreRule{rule}
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// ignoreMatchFilter creates an ignore filter based on the provided IgnoredMatches to filter out "the same"
+// vulnerabilities reported by other matchers based on overlapping file locations
+func ignoredMatchFilter(ignores []match.IgnoredMatch) match.IgnoreFilter {
+ out := ignoreRulesByLocation{locationToIgnoreRules: map[string][]match.IgnoreRule{}}
+ for _, ignore := range ignores {
+ // TODO should this be syftPkg.FileOwner interface or similar?
+ if m, ok := ignore.Package.Metadata.(pkg.ApkMetadata); ok {
+ for _, f := range m.Files {
+ out.locationToIgnoreRules[f.Path] = append(out.locationToIgnoreRules[f.Path], ignore.AppliedIgnoreRules...)
+ }
+ }
+ }
+ return out
+}
+
+func displayPackage(p pkg.Package) string {
+ if p.PURL != "" {
+ return p.PURL
+ }
+ ty := p.Type
+ if p.Type == "" {
+ ty = "unknown"
+ }
+
+ return fmt.Sprintf("%s@%s (type=%s)", p.Name, p.Version, ty)
+}
+
+func ignoredMatchesDiff(subject []match.IgnoredMatch, other []match.IgnoredMatch) []match.IgnoredMatch {
+ // TODO(alex): the downside with this implementation is that it does not account for the same ignored match being
+ // ignored for different reasons (the appliedIgnoreRules field).
+
+ otherMap := make(map[match.Fingerprint]struct{})
+ for _, a := range other {
+ otherMap[a.Fingerprint()] = struct{}{}
+ }
+
+ var diff []match.IgnoredMatch
+ for _, b := range subject {
+ if _, ok := otherMap[b.Fingerprint()]; !ok {
+ diff = append(diff, b)
+ }
+ }
+
+ return diff
+}
+
+func newMatcherIndex(matchers []match.Matcher) (map[syftPkg.Type][]match.Matcher, match.Matcher) {
+ matcherIndex := make(map[syftPkg.Type][]match.Matcher)
+ var defaultMatcher match.Matcher
+ for _, m := range matchers {
+ if m.Type() == match.StockMatcher {
+ defaultMatcher = m
+ continue
+ }
+ for _, t := range m.PackageTypes() {
+ if _, ok := matcherIndex[t]; !ok {
+ matcherIndex[t] = make([]match.Matcher, 0)
+ }
+
+ matcherIndex[t] = append(matcherIndex[t], m)
+ log.Tracef("adding matcher: %+v", t)
+ }
+ }
+
+ return matcherIndex, defaultMatcher
+}
+
func isCVE(id string) bool {
return strings.HasPrefix(strings.ToLower(id), "cve-")
}
-func HasSeverityAtOrAbove(store vulnerability.MetadataProvider, severity vulnerability.Severity, matches match.Matches) bool {
+func hasSeverityAtOrAbove(store vulnerability.MetadataProvider, severity vulnerability.Severity, matches match.Matches) bool {
if severity == vulnerability.UnknownSeverity {
return false
}
for m := range matches.Enumerate() {
- metadata, err := store.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
+ metadata, err := store.VulnerabilityMetadata(m.Vulnerability.Reference)
if err != nil {
continue
}
@@ -151,3 +420,154 @@ func HasSeverityAtOrAbove(store vulnerability.MetadataProvider, severity vulnera
}
return false
}
+
+func logListSummary(vl *monitorWriter) {
+ log.Infof("found %d vulnerability matches across %d packages", vl.MatchesDiscovered.Current(), vl.PackagesProcessed.Current())
+ log.Debugf(" ├── fixed: %d", vl.Fixed.Current())
+ log.Debugf(" ├── ignored: %d (due to user-provided rule)", vl.Ignored.Current())
+ log.Debugf(" ├── dropped: %d (due to hard-coded correction)", vl.Dropped.Current())
+ log.Debugf(" └── matched: %d", vl.MatchesDiscovered.Current())
+
+ var unknownCount int64
+ if count, ok := vl.BySeverity[vulnerability.UnknownSeverity]; ok {
+ unknownCount = count.Current()
+ }
+ log.Debugf(" ├── %s: %d", vulnerability.UnknownSeverity.String(), unknownCount)
+
+ allSeverities := vulnerability.AllSeverities()
+ for idx, sev := range allSeverities {
+ arm := selectArm(idx, len(allSeverities))
+ log.Debugf(" %s %s: %d", arm, sev.String(), vl.BySeverity[sev].Current())
+ }
+}
+
+func updateVulnerabilityList(mon *monitorWriter, matches []match.Match, ignores []match.IgnoredMatch, dropped []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider) {
+ for _, m := range matches {
+ metadata, err := metadataProvider.VulnerabilityMetadata(m.Vulnerability.Reference)
+ if err != nil || metadata == nil {
+ mon.BySeverity[vulnerability.UnknownSeverity].Increment()
+ continue
+ }
+
+ sevManualProgress, ok := mon.BySeverity[vulnerability.ParseSeverity(metadata.Severity)]
+ if !ok {
+ mon.BySeverity[vulnerability.UnknownSeverity].Increment()
+ continue
+ }
+ sevManualProgress.Increment()
+
+ if m.Vulnerability.Fix.State == vulnerability.FixStateFixed {
+ mon.Fixed.Increment()
+ }
+ }
+
+ mon.Ignored.Add(int64(len(ignores)))
+ mon.Dropped.Add(int64(len(dropped)))
+}
+
+func logPackageMatches(p pkg.Package, matches []match.Match) {
+ if len(matches) == 0 {
+ return
+ }
+
+ log.WithFields("package", displayPackage(p)).Debugf("found %d vulnerabilities", len(matches))
+ for idx, m := range matches {
+ arm := selectArm(idx, len(matches))
+ log.WithFields("vuln", m.Vulnerability.ID, "namespace", m.Vulnerability.Namespace).Debugf(" %s", arm)
+ }
+}
+
+func selectArm(idx, total int) string {
+ if idx == total-1 {
+ return leaf
+ }
+ return branch
+}
+
+func logExplicitDroppedPackageMatches(p pkg.Package, ignored []match.IgnoredMatch) {
+ if len(ignored) == 0 {
+ return
+ }
+
+ log.WithFields("package", displayPackage(p)).Debugf("dropped %d vulnerability matches due to hard-coded correction", len(ignored))
+ for idx, i := range ignored {
+ arm := selectArm(idx, len(ignored))
+
+ log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules)).Debugf(" %s", arm)
+ }
+}
+
+func logIgnoredMatches(ignored []match.IgnoredMatch) {
+ if len(ignored) == 0 {
+ return
+ }
+
+ log.Infof("ignored %d vulnerability matches", len(ignored))
+ for idx, i := range ignored {
+ arm := selectArm(idx, len(ignored))
+
+ log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules), "package", displayPackage(i.Package)).Debugf(" %s", arm)
+ }
+}
+
+type monitorWriter struct {
+ PackagesProcessed *progress.Manual
+ MatchesDiscovered *progress.Manual
+ Fixed *progress.Manual
+ Ignored *progress.Manual
+ Dropped *progress.Manual
+ BySeverity map[vulnerability.Severity]*progress.Manual
+}
+
+func newMonitor(pkgCount int) (monitorWriter, monitor.Matching) {
+ manualBySev := make(map[vulnerability.Severity]*progress.Manual)
+ for _, severity := range vulnerability.AllSeverities() {
+ manualBySev[severity] = progress.NewManual(-1)
+ }
+ manualBySev[vulnerability.UnknownSeverity] = progress.NewManual(-1)
+
+ m := monitorWriter{
+ PackagesProcessed: progress.NewManual(int64(pkgCount)),
+ MatchesDiscovered: progress.NewManual(-1),
+ Fixed: progress.NewManual(-1),
+ Ignored: progress.NewManual(-1),
+ Dropped: progress.NewManual(-1),
+ BySeverity: manualBySev,
+ }
+
+ monitorableBySev := make(map[vulnerability.Severity]progress.Monitorable)
+ for sev, manual := range manualBySev {
+ monitorableBySev[sev] = manual
+ }
+
+ return m, monitor.Matching{
+ PackagesProcessed: m.PackagesProcessed,
+ MatchesDiscovered: m.MatchesDiscovered,
+ Fixed: m.Fixed,
+ Ignored: m.Ignored,
+ Dropped: m.Dropped,
+ BySeverity: monitorableBySev,
+ }
+}
+
+func (m *monitorWriter) SetCompleted() {
+ m.PackagesProcessed.SetCompleted()
+ m.MatchesDiscovered.SetCompleted()
+ m.Fixed.SetCompleted()
+ m.Ignored.SetCompleted()
+ m.Dropped.SetCompleted()
+ for _, v := range m.BySeverity {
+ v.SetCompleted()
+ }
+}
+
+func trackMatcher(pkgCount int) *monitorWriter {
+ writer, reader := newMonitor(pkgCount)
+
+ bus.Publish(partybus.Event{
+ Type: event.VulnerabilityScanningStarted,
+ Value: reader,
+ })
+
+ return &writer
+}
diff --git a/grype/vulnerability_matcher_test.go b/grype/vulnerability_matcher_test.go
index 0e856170698..b9a7010578e 100644
--- a/grype/vulnerability_matcher_test.go
+++ b/grype/vulnerability_matcher_test.go
@@ -1,202 +1,126 @@
package grype
import (
+ "errors"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/wagoodman/go-partybus"
- "github.com/anchore/grype/grype/db"
- grypeDB "github.com/anchore/grype/grype/db/v5"
+ "github.com/anchore/grype/grype/distro"
+ "github.com/anchore/grype/grype/event"
+ "github.com/anchore/grype/grype/event/monitor"
"github.com/anchore/grype/grype/grypeerr"
"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"
- "github.com/anchore/grype/grype/search"
- "github.com/anchore/grype/grype/store"
"github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vex"
"github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
+ "github.com/anchore/grype/internal/bus"
"github.com/anchore/syft/syft/cpe"
- "github.com/anchore/syft/syft/linux"
+ "github.com/anchore/syft/syft/file"
syftPkg "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/source"
)
-type ack interface {
- grypeDB.VulnerabilityStoreReader
- grypeDB.VulnerabilityMetadataStoreReader
- grypeDB.VulnerabilityMatchExclusionStoreReader
-}
-
-var _ ack = (*mockStore)(nil)
-
-type mockStore struct {
- vulnerabilities map[string]map[string][]grypeDB.Vulnerability
- metadata map[string]map[string]*grypeDB.VulnerabilityMetadata
-}
-
-func (d *mockStore) GetVulnerabilityMatchExclusion(id string) ([]grypeDB.VulnerabilityMatchExclusion, error) {
- //panic("implement me")
- return nil, nil
-}
-
-func newMockStore() *mockStore {
- d := mockStore{
- vulnerabilities: make(map[string]map[string][]grypeDB.Vulnerability),
- metadata: make(map[string]map[string]*grypeDB.VulnerabilityMetadata),
- }
- d.stub()
- return &d
-}
-
-func (d *mockStore) stub() {
- // METADATA /////////////////////////////////////////////////////////////////////////////////
- d.metadata["CVE-2014-fake-1"] = map[string]*grypeDB.VulnerabilityMetadata{
- "debian:distro:debian:8": {
- ID: "CVE-2014-fake-1",
- Namespace: "debian:distro:debian:8",
- Severity: "medium",
- },
- }
-
- d.metadata["GHSA-2014-fake-3"] = map[string]*grypeDB.VulnerabilityMetadata{
- "github:language:ruby": {
- ID: "GHSA-2014-fake-3",
- Namespace: "github:language:ruby",
- Severity: "medium",
+func testVulnerabilities() []vulnerability.Vulnerability {
+ return []vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-1",
+ Namespace: "debian:distro:debian:8",
+ Internal: vulnerability.Metadata{
+ Severity: "medium",
+ },
+ },
+ PackageName: "neutron",
+ Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
},
- }
-
- d.metadata["CVE-2014-fake-3"] = map[string]*grypeDB.VulnerabilityMetadata{
- "nvd:cpe": {
- ID: "CVE-2014-fake-3",
- Namespace: "nvd:cpe",
- Severity: "critical",
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2013-fake-2",
+ Namespace: "debian:distro:debian:8",
+ },
+ PackageName: "neutron",
+ Constraint: version.MustGetConstraint("< 2013.0.2-1", version.DebFormat),
},
- }
-
- // VULNERABILITIES ///////////////////////////////////////////////////////////////////////////
- d.vulnerabilities["debian:distro:debian:8"] = map[string][]grypeDB.Vulnerability{
- "neutron": {
- {
- PackageName: "neutron",
- Namespace: "debian:distro:debian:8",
- VersionConstraint: "< 2014.1.3-6",
- ID: "CVE-2014-fake-1",
- VersionFormat: "deb",
+ {
+ Reference: vulnerability.Reference{
+ ID: "GHSA-2014-fake-3",
+ Namespace: "github:language:ruby",
+ Internal: vulnerability.Metadata{
+ Severity: "medium",
+ },
},
- {
- PackageName: "neutron",
- Namespace: "debian:distro:debian:8",
- VersionConstraint: "< 2013.0.2-1",
- ID: "CVE-2013-fake-2",
- VersionFormat: "deb",
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ RelatedVulnerabilities: []vulnerability.Reference{
+ {
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ },
},
},
- }
- d.vulnerabilities["github:language:ruby"] = map[string][]grypeDB.Vulnerability{
- "activerecord": {
- {
- PackageName: "activerecord",
- Namespace: "github:language:ruby",
- VersionConstraint: "< 3.7.6",
- ID: "GHSA-2014-fake-3",
- VersionFormat: "unknown",
- RelatedVulnerabilities: []grypeDB.VulnerabilityReference{
- {
- ID: "CVE-2014-fake-3",
- Namespace: "nvd:cpe",
- },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ Internal: vulnerability.Metadata{
+ Severity: "critical",
},
},
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""),
+ },
},
- }
- d.vulnerabilities["nvd:cpe"] = map[string][]grypeDB.Vulnerability{
- "activerecord": {
- {
- PackageName: "activerecord",
- Namespace: "nvd:cpe",
- VersionConstraint: "< 3.7.6",
- ID: "CVE-2014-fake-3",
- VersionFormat: "unknown",
- CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
- },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-4",
+ Namespace: "nvd:cpe",
},
- {
- PackageName: "activerecord",
- Namespace: "nvd:cpe",
- VersionConstraint: "< 3.7.4",
- ID: "CVE-2014-fake-4",
- VersionFormat: "unknown",
- CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*",
- },
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
},
- {
- PackageName: "activerecord",
- Namespace: "nvd:cpe",
- VersionConstraint: "= 4.0.1",
- ID: "CVE-2014-fake-5",
- VersionFormat: "unknown",
- CPEs: []string{
- "cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*",
- },
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-5",
+ Namespace: "nvd:cpe",
},
- {
- PackageName: "activerecord",
- Namespace: "nvd:cpe",
- VersionConstraint: "< 98SP3",
- ID: "CVE-2014-fake-6",
- VersionFormat: "unknown",
- CPEs: []string{
- "cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*",
- },
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("= 4.0.1", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*", ""),
+ },
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-6",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 98SP3", version.UnknownFormat),
+ CPEs: []cpe.CPE{
+ cpe.Must("cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", ""),
},
},
}
}
-func (d *mockStore) GetVulnerabilityMetadata(id, namespace string) (*grypeDB.VulnerabilityMetadata, error) {
- return d.metadata[id][namespace], nil
-}
-
-func (d *mockStore) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) {
- panic("implement me")
-}
-
-func (d *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
- var results []grypeDB.Vulnerability
- for _, vulns := range d.vulnerabilities[namespace] {
- for _, vuln := range vulns {
- if vuln.ID == id {
- results = append(results, vuln)
- }
- }
- }
- return results, nil
-}
-
-func (d *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
- return d.vulnerabilities[namespace][name], nil
-}
-
-func (d *mockStore) GetAllVulnerabilities() (*[]grypeDB.Vulnerability, error) {
- panic("implement me")
-}
-
-func (d *mockStore) GetVulnerabilityNamespaces() ([]string, error) {
- keys := make([]string, 0, len(d.vulnerabilities))
- for k := range d.vulnerabilities {
- keys = append(keys, k)
- }
-
- return keys, nil
-}
-
func Test_HasSeverityAtOrAbove(t *testing.T) {
thePkg := pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -208,8 +132,10 @@ func Test_HasSeverityAtOrAbove(t *testing.T) {
matches := match.NewMatches()
matches.Add(match.Match{
Vulnerability: vulnerability.Vulnerability{
- ID: "CVE-2014-fake-1",
- Namespace: "debian:distro:debian:8",
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-1",
+ Namespace: "debian:distro:debian:8",
+ },
},
Package: thePkg,
Details: match.Details{
@@ -251,7 +177,7 @@ func Test_HasSeverityAtOrAbove(t *testing.T) {
},
}
- metadataProvider := db.NewVulnerabilityMetadataProvider(newMockStore())
+ metadataProvider := mock.VulnerabilityProvider(testVulnerabilities()...)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -264,7 +190,7 @@ func Test_HasSeverityAtOrAbove(t *testing.T) {
failOnSeverity = sev
}
- actual := HasSeverityAtOrAbove(metadataProvider, failOnSeverity, test.matches)
+ actual := hasSeverityAtOrAbove(metadataProvider, failOnSeverity, test.matches)
if test.expectedResult != actual {
t.Errorf("expected: %v got : %v", test.expectedResult, actual)
@@ -274,14 +200,7 @@ func Test_HasSeverityAtOrAbove(t *testing.T) {
}
func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
- mkStr := newMockStore()
- vp, err := db.NewVulnerabilityProvider(mkStr)
- require.NoError(t, err)
- str := store.Store{
- Provider: vp,
- MetadataProvider: db.NewVulnerabilityMetadataProvider(mkStr),
- ExclusionProvider: db.NewMatchExclusionProvider(mkStr),
- }
+ vp := mock.VulnerabilityProvider(testVulnerabilities()...)
neutron2013Pkg := pkg.Package{
ID: pkg.ID(uuid.NewString()),
@@ -291,7 +210,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
}
mustCPE := func(c string) cpe.CPE {
- cp, err := cpe.New(c)
+ cp, err := cpe.New(c, "")
if err != nil {
t.Fatal(err)
}
@@ -310,11 +229,11 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
}
type fields struct {
- Store store.Store
- Matchers []matcher.Matcher
+ Matchers []match.Matcher
IgnoreRules []match.IgnoreRule
FailSeverity *vulnerability.Severity
NormalizeByCVE bool
+ VexProcessor *vex.Processor
}
type args struct {
pkgs []pkg.Package
@@ -332,7 +251,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
{
name: "no matches",
fields: fields{
- Store: str,
Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
},
args: args{
@@ -345,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",
},
},
},
@@ -355,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{
@@ -363,18 +280,21 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
neutron2013Pkg,
},
context: pkg.Context{
- Distro: &linux.Release{
- ID: "debian",
- VersionID: "8",
+ Distro: &distro.Distro{
+ Type: "debian",
+ Version: "8",
},
},
},
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
- Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
- ID: "CVE-2014-fake-1",
- Namespace: "debian:distro:debian:8",
+ PackageName: "neutron",
+ Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-1",
+ Namespace: "debian:distro:debian:8",
+ },
PackageQualifiers: []qualifier.Qualifier{},
CPEs: []cpe.CPE{},
Advisories: []vulnerability.Advisory{},
@@ -404,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
@@ -416,18 +335,21 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
neutron2013Pkg,
},
context: pkg.Context{
- Distro: &linux.Release{
- ID: "debian",
- VersionID: "8",
+ Distro: &distro.Distro{
+ Type: "debian",
+ Version: "8",
},
},
},
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
- Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
- ID: "CVE-2014-fake-1",
- Namespace: "debian:distro:debian:8",
+ PackageName: "neutron",
+ Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-1",
+ Namespace: "debian:distro:debian:8",
+ },
PackageQualifiers: []qualifier.Qualifier{},
CPEs: []cpe.CPE{},
Advisories: []vulnerability.Advisory{},
@@ -454,10 +376,91 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantIgnoredMatches: nil,
wantErr: grypeerr.ErrAboveSeverityThreshold,
},
+ {
+ name: "pass on severity threshold with VEX",
+ fields: fields{
+ Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
+ FailSeverity: func() *vulnerability.Severity {
+ x := vulnerability.LowSeverity
+ return &x
+ }(),
+ VexProcessor: vex.NewProcessor(vex.ProcessorOptions{
+ Documents: []string{
+ "vex/testdata/vex-docs/openvex-debian.json",
+ },
+ IgnoreRules: []match.IgnoreRule{
+ {
+ VexStatus: "fixed",
+ },
+ },
+ }),
+ },
+ args: args{
+ pkgs: []pkg.Package{
+ neutron2013Pkg,
+ },
+ context: pkg.Context{
+ Source: &source.Description{
+ Name: "debian",
+ Version: "2013.1.1-1",
+ Metadata: source.ImageMetadata{
+ RepoDigests: []string{
+ "debian@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
+ },
+ },
+ },
+ Distro: &distro.Distro{
+ Type: "debian",
+ Version: "8",
+ },
+ },
+ },
+ wantMatches: match.NewMatches(),
+ wantIgnoredMatches: []match.IgnoredMatch{
+ {
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Namespace: "vex",
+ VexStatus: "fixed",
+ },
+ },
+ Match: match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ PackageName: "neutron",
+ Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-1",
+ Namespace: "debian:distro:debian:8",
+ },
+ PackageQualifiers: []qualifier.Qualifier{},
+ CPEs: []cpe.CPE{},
+ Advisories: []vulnerability.Advisory{},
+ },
+ Package: neutron2013Pkg,
+ Details: match.Details{
+ {
+ Type: match.ExactDirectMatch,
+ SearchedBy: map[string]any{
+ "distro": map[string]string{"type": "debian", "version": "8"},
+ "namespace": "debian:distro:debian:8",
+ "package": map[string]string{"name": "neutron", "version": "2013.1.1-1"},
+ },
+ Found: map[string]any{
+ "versionConstraint": "< 2014.1.3-6 (deb)",
+ "vulnerabilityID": "CVE-2014-fake-1",
+ },
+ Matcher: "dpkg-matcher",
+ Confidence: 1,
+ },
+ },
+ },
+ },
+ },
+ wantErr: nil,
+ },
{
name: "matches by exact-direct match (language)",
fields: fields{
- Store: str,
Matchers: matcher.NewDefaultMatchers(matcher.Config{
Ruby: ruby.MatcherConfig{
UseCPEs: true,
@@ -473,9 +476,12 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
- Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
- ID: "CVE-2014-fake-3",
- Namespace: "nvd:cpe",
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ },
CPEs: []cpe.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
@@ -486,17 +492,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
Details: match.Details{
{
Type: match.CPEMatch,
- SearchedBy: search.CPEParameters{
+ SearchedBy: match.CPEParameters{
Namespace: "nvd:cpe",
CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
+ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
},
- Package: search.CPEPackageParameter{
+ Package: match.CPEPackageParameter{
Name: "activerecord",
Version: "3.7.5",
},
},
- Found: search.CPEResult{
+ Found: match.CPEResult{
VulnerabilityID: "CVE-2014-fake-3",
VersionConstraint: "< 3.7.6 (unknown)",
CPEs: []string{
@@ -510,9 +516,12 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
match.Match{
Vulnerability: vulnerability.Vulnerability{
- Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
- ID: "GHSA-2014-fake-3",
- Namespace: "github:language:ruby",
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "GHSA-2014-fake-3",
+ Namespace: "github:language:ruby",
+ },
RelatedVulnerabilities: []vulnerability.Reference{
{
ID: "CVE-2014-fake-3",
@@ -548,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{
@@ -567,9 +575,12 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
- Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
- ID: "CVE-2014-fake-3",
- Namespace: "nvd:cpe",
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ },
CPEs: []cpe.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
@@ -600,17 +611,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
{
Type: match.CPEMatch,
- SearchedBy: search.CPEParameters{
+ SearchedBy: match.CPEParameters{
Namespace: "nvd:cpe",
CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
+ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
},
- Package: search.CPEPackageParameter{
+ Package: match.CPEPackageParameter{
Name: "activerecord",
Version: "3.7.5",
},
},
- Found: search.CPEResult{
+ Found: match.CPEResult{
VulnerabilityID: "CVE-2014-fake-3",
VersionConstraint: "< 3.7.6 (unknown)",
CPEs: []string{
@@ -629,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{
@@ -653,9 +663,12 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
- Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
- ID: "CVE-2014-fake-3",
- Namespace: "nvd:cpe",
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ },
CPEs: []cpe.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
@@ -666,17 +679,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
Details: match.Details{
{
Type: match.CPEMatch,
- SearchedBy: search.CPEParameters{
+ SearchedBy: match.CPEParameters{
Namespace: "nvd:cpe",
CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
+ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
},
- Package: search.CPEPackageParameter{
+ Package: match.CPEPackageParameter{
Name: "activerecord",
Version: "3.7.5",
},
},
- Found: search.CPEResult{
+ Found: match.CPEResult{
VulnerabilityID: "CVE-2014-fake-3",
VersionConstraint: "< 3.7.6 (unknown)",
CPEs: []string{
@@ -690,11 +703,55 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
),
wantErr: nil,
+ wantIgnoredMatches: []match.IgnoredMatch{
+ {
+ Match: match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ },
+ CPEs: []cpe.CPE{},
+ PackageQualifiers: []qualifier.Qualifier{},
+ Advisories: []vulnerability.Advisory{},
+ RelatedVulnerabilities: []vulnerability.Reference{
+ {
+ ID: "GHSA-2014-fake-3",
+ Namespace: "github:language:ruby",
+ },
+ },
+ },
+ Package: activerecordPkg,
+ Details: match.Details{
+ {
+ Type: match.ExactDirectMatch,
+ SearchedBy: map[string]any{
+ "language": "ruby",
+ "namespace": "github:language:ruby",
+ "package": map[string]string{"name": "activerecord", "version": "3.7.5"},
+ },
+ Found: map[string]any{
+ "versionConstraint": "< 3.7.6 (unknown)",
+ "vulnerabilityID": "GHSA-2014-fake-3",
+ },
+ Matcher: "ruby-gem-matcher",
+ Confidence: 1,
+ },
+ },
+ },
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Vulnerability: "GHSA-2014-fake-3",
+ },
+ },
+ },
+ },
},
{
name: "normalize by cve -- ignore CVE",
fields: fields{
- Store: str,
Matchers: matcher.NewDefaultMatchers(
matcher.Config{
Ruby: ruby.MatcherConfig{
@@ -717,6 +774,54 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
wantMatches: match.NewMatches(),
wantIgnoredMatches: []match.IgnoredMatch{
+ {
+ Match: match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ },
+ CPEs: []cpe.CPE{
+ mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
+ },
+ PackageQualifiers: []qualifier.Qualifier{},
+ Advisories: []vulnerability.Advisory{},
+ RelatedVulnerabilities: nil,
+ },
+ Package: activerecordPkg,
+ Details: match.Details{
+ {
+ Type: match.CPEMatch,
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
+ },
+ Package: match.CPEPackageParameter{
+ Name: "activerecord",
+ Version: "3.7.5",
+ },
+ },
+ Found: match.CPEResult{
+ VulnerabilityID: "CVE-2014-fake-3",
+ VersionConstraint: "< 3.7.6 (unknown)",
+ CPEs: []string{
+ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
+ },
+ },
+ Matcher: "ruby-gem-matcher",
+ Confidence: 0.9,
+ },
+ },
+ },
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Vulnerability: "CVE-2014-fake-3",
+ },
+ },
+ },
{
AppliedIgnoreRules: []match.IgnoreRule{
{
@@ -725,9 +830,12 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
- Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
- ID: "CVE-2014-fake-3",
- Namespace: "nvd:cpe",
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ },
CPEs: []cpe.CPE{},
PackageQualifiers: []qualifier.Qualifier{},
Advisories: []vulnerability.Advisory{},
@@ -763,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,
@@ -783,9 +890,12 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
- Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
- ID: "GHSA-2014-fake-3",
- Namespace: "github:language:ruby",
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "GHSA-2014-fake-3",
+ Namespace: "github:language:ruby",
+ },
RelatedVulnerabilities: []vulnerability.Reference{
{
ID: "CVE-2014-fake-3",
@@ -824,9 +934,12 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
- Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
- ID: "CVE-2014-fake-3",
- Namespace: "nvd:cpe",
+ PackageName: "activerecord",
+ Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
+ Reference: vulnerability.Reference{
+ ID: "CVE-2014-fake-3",
+ Namespace: "nvd:cpe",
+ },
CPEs: []cpe.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
@@ -837,17 +950,17 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
Details: match.Details{
{
Type: match.CPEMatch,
- SearchedBy: search.CPEParameters{
+ SearchedBy: match.CPEParameters{
Namespace: "nvd:cpe",
CPEs: []string{
- "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
+ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
},
- Package: search.CPEPackageParameter{
+ Package: match.CPEPackageParameter{
Name: "activerecord",
Version: "3.7.5",
},
},
- Found: search.CPEResult{
+ Found: match.CPEResult{
VulnerabilityID: "CVE-2014-fake-3",
VersionConstraint: "< 3.7.6 (unknown)",
CPEs: []string{
@@ -864,15 +977,22 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantErr: nil,
},
}
+
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &VulnerabilityMatcher{
- Store: tt.fields.Store,
- Matchers: tt.fields.Matchers,
- IgnoreRules: tt.fields.IgnoreRules,
- FailSeverity: tt.fields.FailSeverity,
- NormalizeByCVE: tt.fields.NormalizeByCVE,
+ VulnerabilityProvider: vp,
+ Matchers: tt.fields.Matchers,
+ IgnoreRules: tt.fields.IgnoreRules,
+ FailSeverity: tt.fields.FailSeverity,
+ NormalizeByCVE: tt.fields.NormalizeByCVE,
+ VexProcessor: tt.fields.VexProcessor,
}
+
+ listener := &busListener{}
+ bus.Set(listener)
+ defer bus.Set(nil)
+
actualMatches, actualIgnoreMatches, err := m.FindMatches(tt.args.pkgs, tt.args.context)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
@@ -883,9 +1003,11 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
}
var opts = []cmp.Option{
+ cmpopts.EquateEmpty(),
cmpopts.IgnoreUnexported(match.Match{}),
cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
- cmpopts.IgnoreFields(pkg.Package{}, "Locations"),
+ cmpopts.IgnoreFields(vulnerability.Reference{}, "Internal"),
+ cmpopts.IgnoreFields(pkg.Package{}, "Locations", "Distro"),
cmpopts.IgnoreUnexported(match.IgnoredMatch{}),
}
@@ -896,6 +1018,410 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
if d := cmp.Diff(tt.wantIgnoredMatches, actualIgnoreMatches, opts...); d != "" {
t.Errorf("FindMatches() ignored matches mismatch [ha!] (-want +got):\n%s", d)
}
+
+ // validate the bus-reported ignored counts are accurate
+ require.Equal(t, int64(len(tt.wantIgnoredMatches)), listener.matching.Ignored.Current())
+ })
+ }
+}
+
+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
+ pkgs []pkg.Package
+ vulns []vulnerability.Vulnerability
+ expectedResult map[string][]string
+ errAssertion assert.ErrorAssertionFunc
+ }{
+ {
+ name: "false positive in wolfi package adds index entry",
+ pkgs: []pkg.Package{
+ {
+ Name: "foo",
+ Distro: &distro.Distro{Type: distro.Wolfi},
+ Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
+ {
+ Path: "/bin/foo-binary",
+ },
+ }},
+ },
+ },
+ vulns: []vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "GHSA-2014-fake-3",
+ Namespace: "wolfi:distro:wolfi:rolling",
+ },
+ PackageName: "foo",
+ Constraint: version.MustGetConstraint("< 0", version.ApkFormat),
+ },
+ },
+ expectedResult: map[string][]string{
+ "/bin/foo-binary": {"GHSA-2014-fake-3"},
+ },
+ errAssertion: assert.NoError,
+ },
+ {
+ name: "false positive in wolfi subpackage adds index entry",
+ pkgs: []pkg.Package{
+ {
+ Name: "subpackage-foo",
+ Distro: &distro.Distro{Type: distro.Wolfi},
+ Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
+ {
+ Path: "/bin/foo-subpackage-binary",
+ },
+ }},
+ Upstreams: []pkg.UpstreamPackage{
+ {
+ Name: "origin-foo",
+ },
+ },
+ },
+ },
+ vulns: []vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "GHSA-2014-fake-3",
+ Namespace: "wolfi:distro:wolfi:rolling",
+ },
+ PackageName: "origin-foo",
+ Constraint: version.MustGetConstraint("< 0", version.ApkFormat),
+ },
+ },
+ expectedResult: map[string][]string{
+ "/bin/foo-subpackage-binary": {"GHSA-2014-fake-3"},
+ },
+ errAssertion: assert.NoError,
+ },
+ {
+ name: "fixed vuln (not a false positive) in wolfi package",
+ pkgs: []pkg.Package{
+ {
+ Name: "foo",
+ Distro: &distro.Distro{Type: distro.Wolfi},
+ Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
+ {
+ Path: "/bin/foo-binary",
+ },
+ }},
+ },
+ },
+ vulns: []vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "GHSA-2014-fake-3",
+ Namespace: "wolfi:distro:wolfi:rolling",
+ },
+ PackageName: "foo",
+ Constraint: version.MustGetConstraint("< 1.2.3-r4", version.ApkFormat),
+ },
+ },
+ expectedResult: map[string][]string{},
+ errAssertion: assert.NoError,
+ },
+ {
+ name: "no vuln data for wolfi package",
+ pkgs: []pkg.Package{
+ {
+ Name: "foo",
+ Distro: &distro.Distro{Type: distro.Wolfi},
+ Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
+ {
+ Path: "/bin/foo-binary",
+ },
+ }},
+ },
+ },
+ vulns: []vulnerability.Vulnerability{},
+ expectedResult: map[string][]string{},
+ errAssertion: assert.NoError,
+ },
+ {
+ name: "no files listed for a wolfi package",
+ pkgs: []pkg.Package{
+ {
+ Name: "foo",
+ Distro: &distro.Distro{Type: distro.Wolfi},
+ Metadata: pkg.ApkMetadata{Files: nil},
+ },
+ },
+ vulns: []vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "GHSA-2014-fake-3",
+ Namespace: "wolfi:distro:wolfi:rolling",
+ },
+ PackageName: "foo",
+ Constraint: version.MustGetConstraint("< 0", version.ApkFormat),
+ },
+ },
+ expectedResult: map[string][]string{},
+ errAssertion: assert.NoError,
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ // create mock vulnerability provider
+ vp := mock.VulnerabilityProvider(tt.vulns...)
+ apkMatcher := &apk.Matcher{}
+
+ var allMatches []match.Match
+ var allIgnores []match.IgnoredMatch
+ for _, p := range tt.pkgs {
+ matches, ignores, err := apkMatcher.Match(vp, p)
+ require.NoError(t, err)
+ allMatches = append(allMatches, matches...)
+ allIgnores = append(allIgnores, ignores...)
+ }
+
+ actualResult := map[string][]string{}
+ for _, ignore := range allIgnores {
+ apkMetadata, ok := ignore.Package.Metadata.(pkg.ApkMetadata)
+ require.True(t, ok)
+ for _, f := range apkMetadata.Files {
+ for _, r := range ignore.AppliedIgnoreRules {
+ actualResult[f.Path] = append(actualResult[f.Path], r.Vulnerability)
+ }
+ }
+ }
+ assert.Equal(t, tt.expectedResult, actualResult)
+ })
+ }
+}
+
+func Test_filterMatchesUsingDistroFalsePositives(t *testing.T) {
+ cases := []struct {
+ name string
+ inputMatches []match.Match
+ fpIndex map[string][]string
+ expected []match.Match
+ }{
+ {
+ name: "no input matches",
+ inputMatches: nil,
+ fpIndex: map[string][]string{
+ "/usr/bin/crane": {"CVE-2014-fake-3"},
+ },
+ expected: nil,
+ },
+ {
+ name: "happy path filtering",
+ inputMatches: []match.Match{
+ {
+ Package: pkg.Package{
+ Name: "crane",
+ Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
+ },
+ Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
+ },
+ },
+ fpIndex: map[string][]string{
+ "/usr/bin/crane": {"CVE-2014-fake-3"},
+ },
+ expected: nil,
+ },
+ {
+ name: "location match but no vulns in FP index",
+ inputMatches: []match.Match{
+ {
+ Package: pkg.Package{
+ Name: "crane",
+ Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
+ },
+ Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
+ },
+ },
+ fpIndex: map[string][]string{
+ "/usr/bin/crane": {},
+ },
+ expected: []match.Match{
+ {
+ Package: pkg.Package{
+ Name: "crane",
+ Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
+ },
+ Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
+ },
+ },
+ },
+ {
+ name: "location match but matched vuln not in FP index",
+ inputMatches: []match.Match{
+ {
+ Package: pkg.Package{
+ Name: "crane",
+ Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
+ },
+ Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
+ },
+ },
+ fpIndex: map[string][]string{
+ "/usr/bin/crane": {"CVE-2016-fake-3"},
+ },
+ expected: []match.Match{
+ {
+ Package: pkg.Package{
+ Name: "crane",
+ Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
+ },
+ Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
+ },
+ },
+ },
+ {
+ name: "empty FP index",
+ inputMatches: []match.Match{
+ {
+ Package: pkg.Package{
+ Name: "crane",
+ Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
+ },
+ Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
+ },
+ },
+ fpIndex: map[string][]string{},
+ expected: []match.Match{
+ {
+ Package: pkg.Package{
+ Name: "crane",
+ Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
+ },
+ Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
+ },
+ },
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ var allIgnores []match.IgnoredMatch
+ for path, cves := range tt.fpIndex {
+ for _, cve := range cves {
+ allIgnores = append(allIgnores, match.IgnoredMatch{
+ Match: match.Match{
+ Package: pkg.Package{
+ Metadata: pkg.ApkMetadata{
+ Files: []pkg.ApkFileRecord{
+ {
+ Path: path,
+ },
+ },
+ },
+ },
+ },
+ AppliedIgnoreRules: []match.IgnoreRule{
+ {
+ Vulnerability: cve,
+ },
+ },
+ })
+ }
+ }
+
+ filter := ignoredMatchFilter(allIgnores)
+
+ actual, _ := match.ApplyIgnoreFilters(tt.inputMatches, filter)
+
+ assert.Equal(t, tt.expected, actual)
+ })
+ }
+}
+
+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
+}
+
+func (b *busListener) Publish(e partybus.Event) {
+ if e.Type == event.VulnerabilityScanningStarted {
+ if m, ok := e.Value.(monitor.Matching); ok {
+ b.matching = m
+ }
+ }
+}
+
+var _ partybus.Publisher = (*busListener)(nil)
diff --git a/install.sh b/install.sh
index 63f8ee4c62e..303bf4738cb 100755
--- a/install.sh
+++ b/install.sh
@@ -2,33 +2,25 @@
# note: we require errors to propagate (don't set -e)
set -u
-PROJECT_NAME="grype"
+PROJECT_NAME=grype
OWNER=anchore
REPO="${PROJECT_NAME}"
GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download
INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
PROGRAM_ARGS=$@
-# do not change the name of this parameter (this must always be backwards compatible)
-DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true}
+# signature verification options
-#
-# usage [script-name]
-#
-usage() (
- this=$1
- cat </dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
+ target_basename=${target##*/}
+ want=$(grep "${target_basename}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
if [ -z "$want" ]; then
- log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
+ log_err "hash_sha256_verify unable to find checksum for '${target}' in '${checksums}'"
return 1
fi
- got=$(hash_sha256 "$TARGET")
+ got=$(hash_sha256 "$target")
if [ "$want" != "$got" ]; then
- log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
+ log_err "hash_sha256_verify checksum for '$target' did not verify ${want} vs $got"
return 1
fi
)
@@ -358,30 +350,71 @@ github_release_tag() (
echo "$tag"
)
-# download_github_release_checksums [release-url-prefix] [name] [version] [output-dir]
+# github_release_asset_url [release-url-prefix] [name] [version] [output-dir] [filename]
#
-# outputs path to the downloaded checksums file
+# outputs the url to the release asset
#
-download_github_release_checksums() (
+github_release_asset_url() (
+ download_url="$1"
+ name="$2"
+ version="$3"
+ filename="$4"
+
+ complete_filename="${name}_${version}_${filename}"
+ complete_url="${download_url}/${complete_filename}"
+
+ echo "${complete_url}"
+)
+
+# download_github_release_checksums_files [release-url-prefix] [name] [version] [output-dir] [filename]
+#
+# outputs path to the downloaded checksums related file
+#
+download_github_release_checksums_files() (
download_url="$1"
name="$2"
version="$3"
output_dir="$4"
+ filename="$5"
- log_trace "download_github_release_checksums(url=${download_url}, name=${name}, version=${version}, output_dir=${output_dir})"
+ log_trace "download_github_release_checksums_files(url=${download_url}, name=${name}, version=${version}, output_dir=${output_dir}, filename=${filename})"
- checksum_filename=${name}_${version}_checksums.txt
- checksum_url=${download_url}/${checksum_filename}
- output_path="${output_dir}/${checksum_filename}"
+ complete_filename="${name}_${version}_${filename}"
+ complete_url=$(github_release_asset_url "${download_url}" "${name}" "${version}" "${filename}")
+ output_path="${output_dir}/${complete_filename}"
- http_download "${output_path}" "${checksum_url}" ""
+ http_download "${output_path}" "${complete_url}" ""
asset_file_exists "${output_path}"
- log_trace "download_github_release_checksums() returned '${output_path}'"
+ log_trace "download_github_release_checksums_files() returned '${output_path}' for file '${complete_filename}'"
echo "${output_path}"
)
+# download_github_release_checksums [release-url-prefix] [name] [version] [output-dir]
+#
+# outputs path to the downloaded checksums file
+#
+download_github_release_checksums() (
+ download_github_release_checksums_files "$@" "checksums.txt"
+)
+
+# github_release_checksums_sig_url [release-url-prefix] [name] [version]
+#
+# outputs the url to the release checksums signature file
+#
+github_release_checksums_sig_url() (
+ github_release_asset_url "$@" "checksums.txt.sig"
+)
+
+# github_release_checksums_cert_url [release-url-prefix] [name] [version]
+#
+# outputs the url to the release checksums certificate file
+#
+github_release_checksums_cert_url() (
+ github_release_asset_url "$@" "checksums.txt.pem"
+)
+
# search_for_asset [checksums-file-path] [name] [os] [arch] [format]
#
# outputs name of the asset to download
@@ -535,7 +568,10 @@ download_and_install_asset() (
format="$8"
binary="$9"
- asset_filepath=$(download_asset "${download_url}" "${download_path}" "${name}" "${os}" "${arch}" "${version}" "${format}")
+ if ! asset_filepath=$(download_asset "${download_url}" "${download_path}" "${name}" "${os}" "${arch}" "${version}" "${format}"); then
+ log_err "could not download asset for os='${os}' arch='${arch}' format='${format}'"
+ return 1
+ fi
# don't continue if we couldn't download an asset
if [ -z "${asset_filepath}" ]; then
@@ -546,6 +582,36 @@ download_and_install_asset() (
install_asset "${asset_filepath}" "${install_path}" "${binary}"
)
+# verify_sign [checksums-file-path] [certificate-reference] [signature-reference] [version]
+#
+# attempts verify the signature of the checksums file from the release workflow in Github Actions run against the main branch.
+#
+verify_sign() {
+ checksums_file=$1
+ cert_reference=$2
+ sig_reference=$3
+
+ log_trace "verifying artifact $1"
+
+ log_file=$(mktemp)
+
+ ${COSIGN_BINARY} \
+ verify-blob "$checksums_file" \
+ --certificate "$cert_reference" \
+ --signature "$sig_reference" \
+ --certificate-identity "https://github.com/${OWNER}/${REPO}/.github/workflows/release.yaml@refs/heads/main" \
+ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" > "${log_file}" 2>&1
+
+ if [ $? -ne 0 ]; then
+ log_err "$(cat "${log_file}")"
+ rm -f "${log_file}"
+ return 1
+ fi
+
+ rm -f "${log_file}"
+}
+
+
# download_asset [release-url-prefix] [download-path] [name] [os] [arch] [version] [format] [binary]
#
# outputs the path to the downloaded asset asset_filepath
@@ -572,6 +638,20 @@ download_asset() (
return 1
fi
+ if [ "$VERIFY_SIGN" = true ]; then
+ checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}")
+ log_trace "checksums signature url: ${checksum_sig_file_url}"
+
+ checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}")
+ log_trace "checksums certificate url: ${checksums_cert_file_url}"
+
+ if ! verify_sign "${checksums_filepath}" "${checksums_cert_file_url}" "${checksum_sig_file_url}"; then
+ log_err "signature verification failed"
+ return 1
+ fi
+ log_info "signature verification succeeded"
+ fi
+
asset_url="${download_url}/${asset_filename}"
asset_filepath="${destination}/${asset_filename}"
http_download "${asset_filepath}" "${asset_url}" ""
@@ -609,6 +689,79 @@ install_asset() (
install "${archive_dir}/${binary}" "${destination}/"
)
+# compare two semver strings. Returns 0 if version1 >= version2, 1 otherwise.
+# Note: pre-release (-) and metadata (+) are not supported.
+compare_semver() {
+ # remove leading 'v' if present
+ version1=${1#v}
+ version2=${2#v}
+
+ IFS=. read -r major1 minor1 patch1 <= '$VERIFY_SIGN_SUPPORTED_VERSION')"
+ log_err "aborting installation"
+ return 1
+ else
+ log_trace "${PROJECT_NAME} release '$version' supports signature verification (>= '$VERIFY_SIGN_SUPPORTED_VERSION')"
+ fi
+
+ # will invoking an earlier version of this script work (considering the -v flag)?
+ if ! compare_semver "$version" "$VERIFY_SIGN_FLAG_VERSION"; then
+ # the -v argument did not always exist, so we cannot be guaranteed that invoking an earlier version of this script
+ # will work (error with "illegal option -v"). However, the user requested signature verification, so we will
+ # attempt to install the application with this version of the script (keeping signature verification).
+ DOWNLOAD_TAG_INSTALL_SCRIPT=false
+ log_debug "provided version install script does not support -v flag (>= '$VERIFY_SIGN_FLAG_VERSION'), using current script for installation"
+ else
+ log_trace "provided version install script supports -v flag (>= '$VERIFY_SIGN_FLAG_VERSION')"
+ fi
+
+ # check to see if the cosign binary is installed
+ if is_command "${COSIGN_BINARY}"; then
+ log_trace "${COSIGN_BINARY} binary is installed"
+ else
+ log_err "signature verification is requested but ${COSIGN_BINARY} binary is not installed (see https://docs.sigstore.dev/system_config/installation/ to install it)"
+ return 1
+ fi
+}
+
main() (
# parse arguments
@@ -616,7 +769,7 @@ main() (
install_dir=${install_dir:-./bin}
# note: never change the program flags or arguments (this must always be backwards compatible)
- while getopts "b:dh?x" arg; do
+ while getopts "b:dvh?x" arg; do
case "$arg" in
b) install_dir="$OPTARG" ;;
d)
@@ -628,11 +781,25 @@ main() (
log_set_priority $log_trace_priority
fi
;;
- h | \?) usage "$0" ;;
+ v) VERIFY_SIGN=true;;
+ h | \?)
+ cat <= the Presenter hint string to use for report formatting and the output file
- File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
- Distro string `yaml:"distro" json:"distro" mapstructure:"distro"` // --distro, specify a distro to explicitly use
- GenerateMissingCPEs bool `yaml:"add-cpes-if-none" json:"add-cpes-if-none" mapstructure:"add-cpes-if-none"` // --add-cpes-if-none, automatically generate CPEs if they are not present in import (e.g. from a 3rd party SPDX document)
- OutputTemplateFile string `yaml:"output-template-file" json:"output-template-file" mapstructure:"output-template-file"` // -t, the template file to use for formatting the final report
- Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
- CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
- OnlyFixed bool `yaml:"only-fixed" json:"only-fixed" mapstructure:"only-fixed"` // only fail if detected vulns have a fix
- OnlyNotFixed bool `yaml:"only-notfixed" json:"only-notfixed" mapstructure:"only-notfixed"` // only fail if detected vulns don't have a fix
- Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override the target platform for a container image
- CliOptions CliOnlyOptions `yaml:"-" json:"-"`
- Search search `yaml:"search" json:"search" mapstructure:"search"`
- Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"`
- Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
- DB database `yaml:"db" json:"db" mapstructure:"db"`
- ExternalSources externalSources `yaml:"external-sources" json:"externalSources" mapstructure:"external-sources"`
- Match matchConfig `yaml:"match" json:"match" mapstructure:"match"`
- Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
- FailOn string `yaml:"fail-on-severity" json:"fail-on-severity" mapstructure:"fail-on-severity"`
- FailOnSeverity *vulnerability.Severity `yaml:"-" json:"-"`
- Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
- Log logging `yaml:"log" json:"log" mapstructure:"log"`
- 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
- 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"`
-}
-
-func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
- config := &Application{
- CliOptions: cliOpts,
- }
- config.loadDefaultValues(v)
-
- return config
-}
-
-func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) (*Application, error) {
- // the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead
- config := newApplicationConfig(v, cliOpts)
-
- if err := readConfig(v, cliOpts.ConfigPath); err != nil && !errors.Is(err, ErrApplicationConfigNotFound) {
- return nil, err
- }
-
- if err := v.Unmarshal(config); err != nil {
- return nil, fmt.Errorf("unable to parse config: %w", err)
- }
- config.ConfigPath = v.ConfigFileUsed()
-
- if err := config.parseConfigValues(); err != nil {
- return nil, fmt.Errorf("invalid application config: %w", err)
- }
-
- return config, nil
-}
-
-// init loads the default configuration values into the viper instance (before the config values are read and parsed).
-func (cfg Application) loadDefaultValues(v *viper.Viper) {
- // set the default values for primitive fields in this struct
- v.SetDefault("check-for-app-update", true)
- v.SetDefault("default-image-pull-source", "")
-
- // for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
- value := reflect.ValueOf(cfg)
- for i := 0; i < value.NumField(); i++ {
- // note: the defaultValueLoader method receiver is NOT a pointer receiver.
- if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok {
- // the field implements defaultValueLoader, call it
- loadable.loadDefaultValues(v)
- }
- }
-}
-
-func (cfg *Application) parseConfigValues() error {
- // parse application config options
- for _, optionFn := range []func() error{
- cfg.parseLogLevelOption,
- cfg.parseFailOnOption,
- } {
- if err := optionFn(); err != nil {
- return err
- }
- }
-
- // parse nested config options
- // for each field in the configuration struct, see if the field implements the parser interface
- // note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address)
- value := reflect.ValueOf(cfg).Elem()
- for i := 0; i < value.NumField(); i++ {
- // note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer.
- if parsable, ok := value.Field(i).Addr().Interface().(parser); ok {
- // the field implements parser, call it
- if err := parsable.parseConfigValues(); err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-func (cfg *Application) parseLogLevelOption() error {
- switch {
- case cfg.Quiet:
- // TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk)
- // we should be able to quiet the console logging and leave file logging alone...
- // ... this will be an enhancement for later
- cfg.Log.Level = logger.DisabledLevel
-
- case cfg.CliOptions.Verbosity > 0:
- verb := cfg.CliOptions.Verbosity
- cfg.Log.Level = logger.LevelFromVerbosity(verb, logger.WarnLevel, logger.InfoLevel, logger.DebugLevel, logger.TraceLevel)
-
- case cfg.Log.Level != "":
- var err error
- cfg.Log.Level, err = logger.LevelFromString(string(cfg.Log.Level))
- if err != nil {
- return err
- }
-
- if logger.IsVerbose(cfg.Log.Level) {
- cfg.Verbosity = 1
- }
- default:
- cfg.Log.Level = logger.WarnLevel
- }
-
- return nil
-}
-
-func (cfg *Application) parseFailOnOption() error {
- if cfg.FailOn != "" {
- failOnSeverity := vulnerability.ParseSeverity(cfg.FailOn)
- if failOnSeverity == vulnerability.UnknownSeverity {
- return fmt.Errorf("bad --fail-on severity value '%s'", cfg.FailOn)
- }
- cfg.FailOnSeverity = &failOnSeverity
- }
- return nil
-}
-
-func (cfg Application) String() string {
- // yaml is pretty human friendly (at least when compared to json)
- appCfgStr, err := yaml.Marshal(&cfg)
-
- if err != nil {
- return err.Error()
- }
-
- return string(appCfgStr)
-}
-
-// readConfig attempts to read the given config path from disk or discover an alternate store location
-func readConfig(v *viper.Viper, configPath string) error {
- var err error
- v.AutomaticEnv()
- v.SetEnvPrefix(internal.ApplicationName)
- // allow for nested options to be specified via environment variables
- // e.g. pod.context = APPNAME_POD_CONTEXT
- v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
-
- // use explicitly the given user config
- if configPath != "" {
- v.SetConfigFile(configPath)
- if err := v.ReadInConfig(); err != nil {
- return fmt.Errorf("unable to read application config=%q : %w", configPath, err)
- }
- // don't fall through to other options if the config path was explicitly provided
- return nil
- }
-
- // start searching for valid configs in order...
-
- // 1. look for ..yaml (in the current directory)
- v.AddConfigPath(".")
- v.SetConfigName("." + internal.ApplicationName)
- if err = v.ReadInConfig(); err == nil {
- return nil
- } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
- return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
- }
-
- // 2. look for ./config.yaml (in the current directory)
- v.AddConfigPath("." + internal.ApplicationName)
- v.SetConfigName("config")
- if err = v.ReadInConfig(); err == nil {
- return nil
- } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
- return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
- }
-
- // 3. look for ~/..yaml
- home, err := homedir.Dir()
- if err == nil {
- v.AddConfigPath(home)
- v.SetConfigName("." + internal.ApplicationName)
- if err = v.ReadInConfig(); err == nil {
- return nil
- } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
- return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
- }
- }
-
- // 4. look for /config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
- v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName))
- for _, dir := range xdg.ConfigDirs {
- v.AddConfigPath(path.Join(dir, internal.ApplicationName))
- }
- v.SetConfigName("config")
- if err = v.ReadInConfig(); err == nil {
- return nil
- } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
- return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
- }
-
- return ErrApplicationConfigNotFound
-}
diff --git a/internal/config/cli_only_options.go b/internal/config/cli_only_options.go
deleted file mode 100644
index 5bb0e49671e..00000000000
--- a/internal/config/cli_only_options.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package config
-
-type CliOnlyOptions struct {
- ConfigPath string
- Verbosity int
-}
diff --git a/internal/config/database.go b/internal/config/database.go
deleted file mode 100644
index a3854297c5a..00000000000
--- a/internal/config/database.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package config
-
-import (
- "path"
- "time"
-
- "github.com/adrg/xdg"
- "github.com/spf13/viper"
-
- "github.com/anchore/grype/grype/db"
- "github.com/anchore/grype/internal"
-)
-
-type database struct {
- Dir string `yaml:"cache-dir" json:"cache-dir" mapstructure:"cache-dir"`
- UpdateURL string `yaml:"update-url" json:"update-url" mapstructure:"update-url"`
- CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"`
- AutoUpdate bool `yaml:"auto-update" json:"auto-update" mapstructure:"auto-update"`
- ValidateByHashOnStart bool `yaml:"validate-by-hash-on-start" json:"validate-by-hash-on-start" mapstructure:"validate-by-hash-on-start"`
- ValidateAge bool `yaml:"validate-age" json:"validate-age" mapstructure:"validate-age"`
- MaxAllowedBuiltAge time.Duration `yaml:"max-allowed-built-age" json:"max-allowed-built-age" mapstructure:"max-allowed-built-age"`
-}
-
-func (cfg database) loadDefaultValues(v *viper.Viper) {
- v.SetDefault("db.cache-dir", path.Join(xdg.CacheHome, internal.ApplicationName, "db"))
- v.SetDefault("db.update-url", internal.DBUpdateURL)
- v.SetDefault("db.ca-cert", "")
- v.SetDefault("db.auto-update", true)
- v.SetDefault("db.validate-by-hash-on-start", false)
- v.SetDefault("db.validate-age", true)
- // After this period (5 days) the db data is considered stale
- v.SetDefault("db.max-allowed-built-age", time.Hour*24*5)
-}
-
-func (cfg database) ToCuratorConfig() db.Config {
- return db.Config{
- DBRootDir: cfg.Dir,
- ListingURL: cfg.UpdateURL,
- CACert: cfg.CACert,
- ValidateByHashOnGet: cfg.ValidateByHashOnStart,
- ValidateAge: cfg.ValidateAge,
- MaxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
- }
-}
diff --git a/internal/config/datasources.go b/internal/config/datasources.go
deleted file mode 100644
index df0dc3c1700..00000000000
--- a/internal/config/datasources.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package config
-
-import (
- "github.com/spf13/viper"
-
- "github.com/anchore/grype/grype/matcher/java"
-)
-
-const (
- defaultMavenBaseURL = "https://search.maven.org/solrsearch/select"
-)
-
-type externalSources struct {
- Enable bool `yaml:"enable" json:"enable" mapstructure:"enable"`
- Maven maven `yaml:"maven" json:"maven" mapsructure:"maven"`
-}
-
-type maven struct {
- SearchUpstreamBySha1 bool `yaml:"search-upstream" json:"searchUpstreamBySha1" mapstructure:"search-maven-upstream"`
- BaseURL string `yaml:"base-url" json:"baseUrl" mapstructure:"base-url"`
-}
-
-func (cfg externalSources) loadDefaultValues(v *viper.Viper) {
- v.SetDefault("external-sources.enable", false)
- v.SetDefault("external-sources.maven.search-maven-upstream", true)
- v.SetDefault("external-sources.maven.base-url", defaultMavenBaseURL)
-}
-
-func (cfg externalSources) ToJavaMatcherConfig() java.ExternalSearchConfig {
- // always respect if global config is disabled
- smu := cfg.Maven.SearchUpstreamBySha1
- if !cfg.Enable {
- smu = cfg.Enable
- }
- return java.ExternalSearchConfig{
- SearchMavenUpstream: smu,
- MavenBaseURL: cfg.Maven.BaseURL,
- }
-}
diff --git a/internal/config/development.go b/internal/config/development.go
deleted file mode 100644
index 4e1e8b01af8..00000000000
--- a/internal/config/development.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package config
-
-import "github.com/spf13/viper"
-
-type development struct {
- ProfileCPU bool `yaml:"profile-cpu" json:"profile-cpu" mapstructure:"profile-cpu"`
- ProfileMem bool `yaml:"profile-mem" json:"profile-mem" mapstructure:"profile-mem"`
-}
-
-func (cfg development) loadDefaultValues(v *viper.Viper) {
- v.SetDefault("dev.profile-cpu", false)
- v.SetDefault("dev.profile-mem", false)
-}
diff --git a/internal/config/logging.go b/internal/config/logging.go
deleted file mode 100644
index bf9ffe37e5b..00000000000
--- a/internal/config/logging.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package config
-
-import (
- "github.com/spf13/viper"
-
- "github.com/anchore/go-logger"
-)
-
-// logging contains all logging-related configuration options available to the user via the application config.
-type logging struct {
- Structured bool `yaml:"structured" json:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings
- Level logger.Level `yaml:"level" json:"level" mapstructure:"level"` // the log level string hint
- FileLocation string `yaml:"file" json:"file" mapstructure:"file"` // the file path to write logs to
-}
-
-func (cfg logging) loadDefaultValues(v *viper.Viper) {
- v.SetDefault("log.structured", false)
- v.SetDefault("log.file", "")
- v.SetDefault("log.level", string(logger.WarnLevel))
-}
diff --git a/internal/config/match.go b/internal/config/match.go
deleted file mode 100644
index e855b77a9b4..00000000000
--- a/internal/config/match.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package config
-
-import (
- "github.com/spf13/viper"
-)
-
-// matchConfig contains all matching-related configuration options available to the user via the application config.
-type matchConfig struct {
- Java matcherConfig `yaml:"java" json:"java" mapstructure:"java"` // settings for the java matcher
- Dotnet matcherConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"` // settings for the dotnet matcher
- Golang matcherConfig `yaml:"golang" json:"golang" mapstructure:"golang"` // settings for the golang matcher
- Javascript matcherConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"` // settings for the javascript matcher
- Python matcherConfig `yaml:"python" json:"python" mapstructure:"python"` // settings for the python matcher
- Ruby matcherConfig `yaml:"ruby" json:"ruby" mapstructure:"ruby"` // settings for the ruby matcher
- Stock matcherConfig `yaml:"stock" json:"stock" mapstructure:"stock"` // settings for the default/stock matcher
-}
-
-type matcherConfig struct {
- UseCPEs bool `yaml:"using-cpes" json:"using-cpes" mapstructure:"using-cpes"` // if CPEs should be used during matching
-}
-
-func (cfg matchConfig) loadDefaultValues(v *viper.Viper) {
- v.SetDefault("match.java.using-cpes", true)
- v.SetDefault("match.dotnet.using-cpes", true)
- v.SetDefault("match.golang.using-cpes", true)
- v.SetDefault("match.javascript.using-cpes", false)
- v.SetDefault("match.python.using-cpes", true)
- v.SetDefault("match.ruby.using-cpes", true)
- v.SetDefault("match.stock.using-cpes", true)
-}
diff --git a/internal/config/registry.go b/internal/config/registry.go
deleted file mode 100644
index b2a89155ed4..00000000000
--- a/internal/config/registry.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package config
-
-import (
- "os"
-
- "github.com/spf13/viper"
-
- "github.com/anchore/stereoscope/pkg/image"
-)
-
-type RegistryCredentials struct {
- Authority string `yaml:"authority" json:"authority" mapstructure:"authority"`
- // IMPORTANT: do not show the username in any YAML/JSON output (sensitive information)
- Username string `yaml:"-" json:"-" mapstructure:"username"`
- // IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
- Password string `yaml:"-" json:"-" mapstructure:"password"`
- // IMPORTANT: do not show the token in any YAML/JSON output (sensitive information)
- Token string `yaml:"-" json:"-" mapstructure:"token"`
-}
-
-type registry struct {
- InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
- InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
- Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
-}
-
-func (cfg registry) loadDefaultValues(v *viper.Viper) {
- v.SetDefault("registry.insecure-skip-tls-verify", false)
- v.SetDefault("registry.insecure-use-http", false)
- v.SetDefault("registry.auth", []RegistryCredentials{})
-}
-
-//nolint:unparam
-func (cfg *registry) parseConfigValues() error {
- // there may be additional credentials provided by env var that should be appended to the set of credentials
- authority, username, password, token :=
- os.Getenv("GRYPE_REGISTRY_AUTH_AUTHORITY"),
- os.Getenv("GRYPE_REGISTRY_AUTH_USERNAME"),
- os.Getenv("GRYPE_REGISTRY_AUTH_PASSWORD"),
- os.Getenv("GRYPE_REGISTRY_AUTH_TOKEN")
-
- if hasNonEmptyCredentials(username, password, token) {
- // note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.
- cfg.Auth = append([]RegistryCredentials{
- {
- Authority: authority,
- Username: username,
- Password: password,
- Token: token,
- },
- }, cfg.Auth...)
- }
- return nil
-}
-
-func hasNonEmptyCredentials(username, password, token string) bool {
- return password != "" && username != "" || token != ""
-}
-
-func (cfg *registry) ToOptions() *image.RegistryOptions {
- var auth = make([]image.RegistryCredentials, len(cfg.Auth))
- for i, a := range cfg.Auth {
- auth[i] = image.RegistryCredentials{
- Authority: a.Authority,
- Username: a.Username,
- Password: a.Password,
- Token: a.Token,
- }
- }
- return &image.RegistryOptions{
- InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
- InsecureUseHTTP: cfg.InsecureUseHTTP,
- Credentials: auth,
- }
-}
diff --git a/internal/config/search.go b/internal/config/search.go
deleted file mode 100644
index 57934a72580..00000000000
--- a/internal/config/search.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package config
-
-import (
- "fmt"
-
- "github.com/spf13/viper"
-
- "github.com/anchore/syft/syft/pkg/cataloger"
- "github.com/anchore/syft/syft/source"
-)
-
-type search struct {
- ScopeOpt source.Scope `yaml:"-" json:"-"`
- Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
- IncludeUnindexedArchives bool `yaml:"unindexed-archives" json:"unindexed-archives" mapstructure:"unindexed-archives"`
- IncludeIndexedArchives bool `yaml:"indexed-archives" json:"indexed-archives" mapstructure:"indexed-archives"`
-}
-
-func (cfg *search) parseConfigValues() error {
- scopeOption := source.ParseScope(cfg.Scope)
- if scopeOption == source.UnknownScope {
- return fmt.Errorf("bad scope value %q", cfg.Scope)
- }
- cfg.ScopeOpt = scopeOption
-
- return nil
-}
-
-func (cfg search) loadDefaultValues(v *viper.Viper) {
- c := cataloger.DefaultSearchConfig()
- v.SetDefault("search.unindexed-archives", c.IncludeUnindexedArchives)
- v.SetDefault("search.indexed-archives", c.IncludeIndexedArchives)
-}
-
-func (cfg search) ToConfig() cataloger.Config {
- return cataloger.Config{
- Search: cataloger.SearchConfig{
- IncludeIndexedArchives: cfg.IncludeIndexedArchives,
- IncludeUnindexedArchives: cfg.IncludeUnindexedArchives,
- Scope: cfg.ScopeOpt,
- },
- }
-}
diff --git a/internal/constants.go b/internal/constants.go
deleted file mode 100644
index ea1f6b7b8c6..00000000000
--- a/internal/constants.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package internal
-
-// note: do not change this
-const ApplicationName = "grype"
-const DBUpdateURL = "https://toolbox-data.anchore.io/grype/databases/listing.json"
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/file/getter.go b/internal/file/getter.go
index 216a0965a70..343d9dde1ad 100644
--- a/internal/file/getter.go
+++ b/internal/file/getter.go
@@ -7,9 +7,12 @@ import (
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-getter/helper/url"
+ "github.com/spf13/afero"
"github.com/wagoodman/go-progress"
+ "github.com/anchore/clio"
"github.com/anchore/grype/internal/stringutil"
+ "github.com/anchore/stereoscope/pkg/file"
)
var (
@@ -32,10 +35,13 @@ type HashiGoGetter struct {
// NewGetter creates and returns a new Getter. Providing an http.Client is optional. If one is provided,
// it will be used for all HTTP(S) getting; otherwise, go-getter's default getters will be used.
-func NewGetter(httpClient *http.Client) *HashiGoGetter {
+func NewGetter(id clio.Identification, httpClient *http.Client) *HashiGoGetter {
return &HashiGoGetter{
httpGetter: getter.HttpGetter{
Client: httpClient,
+ Header: http.Header{
+ "User-Agent": []string{fmt.Sprintf("%v %v", id.Name, id.Version)},
+ },
},
}
}
@@ -107,14 +113,33 @@ func withProgress(monitor *progress.Manual) func(client *getter.Client) error {
}
func mapToGetterClientOptions(monitors []*progress.Manual) []getter.ClientOption {
- // TODO: This function is no longer needed once a generic `map` method is available.
-
var result []getter.ClientOption
for _, monitor := range monitors {
result = append(result, withProgress(monitor))
}
+ // derived from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress.go#L23-L63
+ fileSizeLimit := int64(5 * file.GB)
+
+ dec := getter.LimitedDecompressors(0, fileSizeLimit)
+ fs := afero.NewOsFs()
+ xzd := &xzDecompressor{
+ FileSizeLimit: fileSizeLimit,
+ Fs: fs,
+ }
+ txzd := &tarXzDecompressor{
+ FilesLimit: 0,
+ FileSizeLimit: fileSizeLimit,
+ Fs: fs,
+ }
+
+ dec["xz"] = xzd
+ dec["tar.xz"] = txzd
+ dec["txz"] = txzd
+
+ result = append(result, getter.WithDecompressors(dec))
+
return result
}
diff --git a/internal/file/getter_test.go b/internal/file/getter_test.go
index f0bdaf90bd3..a8f47ce816b 100644
--- a/internal/file/getter_test.go
+++ b/internal/file/getter_test.go
@@ -14,6 +14,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+
+ "github.com/anchore/clio"
)
func TestGetter_GetFile(t *testing.T) {
@@ -45,7 +47,7 @@ func TestGetter_GetFile(t *testing.T) {
tc.prepareClient(httpClient)
}
- getter := NewGetter(httpClient)
+ getter := NewGetter(testID, httpClient)
requestURL := createRequestURL(t, server, requestPath)
tempDir := t.TempDir()
@@ -72,7 +74,7 @@ func TestGetter_GetToDir_FilterNonArchivesWired(t *testing.T) {
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
- test.assert(t, NewGetter(nil).GetToDir(t.TempDir(), test.source))
+ test.assert(t, NewGetter(testID, nil).GetToDir(t.TempDir(), test.source))
})
}
}
@@ -138,7 +140,7 @@ func TestGetter_GetToDir_CertConcerns(t *testing.T) {
tc.prepareClient(httpClient)
}
- getter := NewGetter(httpClient)
+ getter := NewGetter(testID, httpClient)
requestURL := createRequestURL(t, server, requestPath)
tempDir := t.TempDir()
@@ -193,6 +195,11 @@ func withResponseForPath(t *testing.T, path string, response []byte) muxOption {
}
}
+var testID = clio.Identification{
+ Name: "test-app",
+ Version: "v0.5.3",
+}
+
func newTestServer(t *testing.T, muxOptions ...muxOption) *httptest.Server {
t.Helper()
diff --git a/internal/file/hasher.go b/internal/file/hasher.go
index c9af0c0b69e..bf1b2b4f4db 100644
--- a/internal/file/hasher.go
+++ b/internal/file/hasher.go
@@ -8,6 +8,7 @@ import (
"io"
"strings"
+ "github.com/OneOfOne/xxhash"
"github.com/spf13/afero"
)
@@ -18,6 +19,9 @@ func ValidateByHash(fs afero.Fs, path, hashStr string) (bool, string, error) {
case strings.HasPrefix(hashStr, "sha256:"):
hashFn = "sha256"
hasher = sha256.New()
+ case strings.HasPrefix(hashStr, "xxh64:"):
+ hashFn = "xxh64"
+ hasher = xxhash.New64()
default:
return false, "", fmt.Errorf("hasher not supported or specified (given: %s)", hashStr)
}
@@ -39,8 +43,12 @@ func HashFile(fs afero.Fs, path string, hasher hash.Hash) (string, error) {
}
defer f.Close()
- if _, err := io.Copy(hasher, f); err != nil {
- return "", fmt.Errorf("failed to hash file '%s': %w", path, err)
+ return HashReader(f, hasher)
+}
+
+func HashReader(reader io.Reader, hasher hash.Hash) (string, error) {
+ if _, err := io.Copy(hasher, reader); err != nil {
+ return "", fmt.Errorf("failed to hash reader: %w", err)
}
return hex.EncodeToString(hasher.Sum(nil)), nil
diff --git a/internal/file/tar.go b/internal/file/tar.go
deleted file mode 100644
index c05e3ecaec3..00000000000
--- a/internal/file/tar.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package file
-
-import (
- "archive/tar"
- "compress/gzip"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "strings"
-)
-
-const (
- _ = iota
- KB = 1 << (10 * iota)
- MB
- GB
- // limit the tar reader to 5GB per file to prevent decompression bomb attacks. Why 5GB? This is somewhat of an
- // arbitrary threshold, however, we need to keep this at at minimum 2GB to accommodate possible grype DB sizes.
- decompressionByteReadLimit = 5 * GB
-)
-
-type errZipSlipDetected struct {
- Prefix string
- JoinArgs []string
-}
-
-func (e *errZipSlipDetected) Error() string {
- return fmt.Sprintf("paths are not allowed to resolve outside of the root prefix (%q). Destination: %q", e.Prefix, e.JoinArgs)
-}
-
-// safeJoin ensures that any destinations do not resolve to a path above the prefix path.
-func safeJoin(prefix string, dest ...string) (string, error) {
- joinResult := filepath.Join(append([]string{prefix}, dest...)...)
- cleanJoinResult := filepath.Clean(joinResult)
- if !strings.HasPrefix(cleanJoinResult, filepath.Clean(prefix)) {
- return "", &errZipSlipDetected{
- Prefix: prefix,
- JoinArgs: dest,
- }
- }
- // why not return the clean path? the called may not be expected it from what should only be a join operation.
- return joinResult, nil
-}
-
-func UnTarGz(dst string, r io.Reader) error {
- gzr, err := gzip.NewReader(r)
- if err != nil {
- return err
- }
- defer gzr.Close()
-
- tr := tar.NewReader(gzr)
-
- for {
- header, err := tr.Next()
-
- switch {
- case err == io.EOF:
- return nil
-
- case err != nil:
- return err
-
- case header == nil:
- continue
- }
-
- target, err := safeJoin(dst, header.Name)
- if err != nil {
- return err
- }
-
- switch header.Typeflag {
- case tar.TypeDir:
- if _, err := os.Stat(target); err != nil {
- if err := os.MkdirAll(target, 0755); err != nil {
- return fmt.Errorf("failed to mkdir (%s): %w", target, err)
- }
- }
-
- case tar.TypeReg:
- f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
- if err != nil {
- return fmt.Errorf("failed to open file (%s): %w", target, err)
- }
-
- if err := copyWithLimits(f, tr, decompressionByteReadLimit, target); err != nil {
- return err
- }
-
- if err = f.Close(); err != nil {
- return fmt.Errorf("failed to close file (%s): %w", target, err)
- }
- }
- }
-}
-
-func copyWithLimits(writer io.Writer, reader io.Reader, byteReadLimit int64, pathInArchive string) error {
- if numBytes, err := io.Copy(writer, io.LimitReader(reader, byteReadLimit)); err != nil {
- return fmt.Errorf("failed to copy file (%s): %w", pathInArchive, err)
- } else if numBytes >= byteReadLimit {
- return fmt.Errorf("failed to copy file (%s): read limit (%d bytes) reached ", pathInArchive, byteReadLimit)
- }
- return nil
-}
diff --git a/internal/file/tar_test.go b/internal/file/tar_test.go
deleted file mode 100644
index f8841e8cb13..00000000000
--- a/internal/file/tar_test.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package file
-
-import (
- "bytes"
- "errors"
- "fmt"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func assertErrorAs(expectedErr interface{}) assert.ErrorAssertionFunc {
- return func(t assert.TestingT, actualErr error, i ...interface{}) bool {
- return errors.As(actualErr, &expectedErr)
- }
-}
-
-func TestSafeJoin(t *testing.T) {
- tests := []struct {
- prefix string
- args []string
- expected string
- errAssertion assert.ErrorAssertionFunc
- }{
- // go cases...
- {
- prefix: "/a/place",
- args: []string{
- "somewhere/else",
- },
- expected: "/a/place/somewhere/else",
- errAssertion: assert.NoError,
- },
- {
- prefix: "/a/place",
- args: []string{
- "somewhere/../else",
- },
- expected: "/a/place/else",
- errAssertion: assert.NoError,
- },
- {
- prefix: "/a/../place",
- args: []string{
- "somewhere/else",
- },
- expected: "/place/somewhere/else",
- errAssertion: assert.NoError,
- },
- // zip slip examples....
- {
- prefix: "/a/place",
- args: []string{
- "../../../etc/passwd",
- },
- expected: "",
- errAssertion: assertErrorAs(&errZipSlipDetected{}),
- },
- {
- prefix: "/a/place",
- args: []string{
- "../",
- "../",
- },
- expected: "",
- errAssertion: assertErrorAs(&errZipSlipDetected{}),
- },
- {
- prefix: "/a/place",
- args: []string{
- "../",
- },
- expected: "",
- errAssertion: assertErrorAs(&errZipSlipDetected{}),
- },
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%+v:%+v", test.prefix, test.args), func(t *testing.T) {
- actual, err := safeJoin(test.prefix, test.args...)
- test.errAssertion(t, err)
- assert.Equal(t, test.expected, actual)
- })
- }
-}
-
-func Test_copyWithLimits(t *testing.T) {
- tests := []struct {
- name string
- input string
- byteReadLimit int64
- pathInArchive string
- expectWritten string
- expectErr bool
- }{
- {
- name: "write bytes",
- input: "something here",
- byteReadLimit: 1000,
- pathInArchive: "dont care",
- expectWritten: "something here",
- expectErr: false,
- },
- {
- name: "surpass upper limit",
- input: "something here",
- byteReadLimit: 11,
- pathInArchive: "dont care",
- expectWritten: "something h",
- expectErr: true,
- },
- // since we want the threshold being reached to be easily detectable, simply reaching the threshold is
- // enough to cause an error. Otherwise surpassing the threshold would be undetectable.
- {
- name: "reach limit exactly",
- input: "something here",
- byteReadLimit: 14,
- pathInArchive: "dont care",
- expectWritten: "something here",
- expectErr: true,
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- writer := &bytes.Buffer{}
- err := copyWithLimits(writer, strings.NewReader(test.input), test.byteReadLimit, test.pathInArchive)
- if (err != nil) != test.expectErr {
- t.Errorf("copyWithLimits() error = %v, want %v", err, test.expectErr)
- return
- } else if err != nil {
- assert.Contains(t, err.Error(), test.pathInArchive)
- }
- assert.Equal(t, test.expectWritten, writer.String())
-
- })
- }
-}
diff --git a/internal/file/tar_xz_decompressor.go b/internal/file/tar_xz_decompressor.go
new file mode 100644
index 00000000000..92fb1661522
--- /dev/null
+++ b/internal/file/tar_xz_decompressor.go
@@ -0,0 +1,220 @@
+package file
+
+import (
+ "archive/tar"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/spf13/afero"
+ "github.com/xi2/xz"
+)
+
+// Note: this is a copy of the TarXzDecompressor from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress_txz.go
+// with the xz lib swapped out (for performance). A few adjustments were made:
+// - refactored to use afero filesystem abstraction
+// - fixed some linting issues
+
+// TarXzDecompressor is an implementation of Decompressor that can
+// decompress tar.xz files.
+type tarXzDecompressor struct {
+ // FileSizeLimit limits the total size of all
+ // decompressed files.
+ //
+ // The zero value means no limit.
+ FileSizeLimit int64
+
+ // FilesLimit limits the number of files that are
+ // allowed to be decompressed.
+ //
+ // The zero value means no limit.
+ FilesLimit int
+
+ Fs afero.Fs
+}
+
+func (d *tarXzDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
+ // If we're going into a directory we should make that first
+ mkdir := dst
+ if !dir {
+ mkdir = filepath.Dir(dst)
+ }
+ if err := d.Fs.MkdirAll(mkdir, mode(0755, umask)); err != nil {
+ return err
+ }
+
+ // File first
+ f, err := d.Fs.Open(src)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ // xz compression is second
+ txzR, err := xz.NewReader(f, 0)
+ if err != nil {
+ return fmt.Errorf("error opening an xz reader for %s: %s", src, err)
+ }
+
+ return untar(d.Fs, txzR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
+}
+
+// untar is a shared helper for untarring an archive. The reader should provide
+// an uncompressed view of the tar archive.
+func untar(fs afero.Fs, input io.Reader, dst, src string, dir bool, umask os.FileMode, fileSizeLimit int64, filesLimit int) error { // nolint:funlen,gocognit
+ tarR := tar.NewReader(input)
+ done := false
+ dirHdrs := []*tar.Header{}
+ now := time.Now()
+
+ var (
+ fileSize int64
+ filesCount int
+ )
+
+ for {
+ if filesLimit > 0 {
+ filesCount++
+ if filesCount > filesLimit {
+ return fmt.Errorf("tar archive contains too many files: %d > %d", filesCount, filesLimit)
+ }
+ }
+
+ hdr, err := tarR.Next()
+ if err == io.EOF {
+ if !done {
+ // Empty archive
+ return fmt.Errorf("empty archive: %s", src)
+ }
+
+ break
+ }
+ if err != nil {
+ return err
+ }
+
+ switch hdr.Typeflag {
+ case tar.TypeSymlink, tar.TypeLink:
+ // to prevent any potential indirect traversal attacks
+ continue
+ case tar.TypeXGlobalHeader, tar.TypeXHeader:
+ // don't unpack extended headers as files
+ continue
+ }
+
+ path := dst
+ if dir {
+ // Disallow parent traversal
+ if containsDotDot(hdr.Name) {
+ return fmt.Errorf("entry contains '..': %s", hdr.Name)
+ }
+
+ path = filepath.Join(path, hdr.Name) // nolint:gosec // hdr.Name is checked above
+ }
+
+ fileInfo := hdr.FileInfo()
+
+ fileSize += fileInfo.Size()
+
+ if fileSizeLimit > 0 && fileSize > fileSizeLimit {
+ return fmt.Errorf("tar archive larger than limit: %d", fileSizeLimit)
+ }
+
+ if fileInfo.IsDir() {
+ if !dir {
+ return fmt.Errorf("expected a single file: %s", src)
+ }
+
+ // A directory, just make the directory and continue unarchiving...
+ if err := fs.MkdirAll(path, mode(0755, umask)); err != nil {
+ return err
+ }
+
+ // Record the directory information so that we may set its attributes
+ // after all files have been extracted
+ dirHdrs = append(dirHdrs, hdr)
+
+ continue
+ }
+ // There is no ordering guarantee that a file in a directory is
+ // listed before the directory
+ dstPath := filepath.Dir(path)
+
+ // Check that the directory exists, otherwise create it
+ if _, err := fs.Stat(dstPath); os.IsNotExist(err) {
+ if err := fs.MkdirAll(dstPath, mode(0755, umask)); err != nil {
+ return err
+ }
+ }
+
+ // We have a file. If we already decoded, then it is an error
+ if !dir && done {
+ return fmt.Errorf("expected a single file, got multiple: %s", src)
+ }
+
+ // Mark that we're done so future in single file mode errors
+ done = true
+
+ // Size limit is tracked using the returned file info.
+ err = copyReader(fs, path, tarR, hdr.FileInfo().Mode(), umask, 0)
+ if err != nil {
+ return err
+ }
+
+ // Set the access and modification time if valid, otherwise default to current time
+ aTime := now
+ mTime := now
+ if hdr.AccessTime.Unix() > 0 {
+ aTime = hdr.AccessTime
+ }
+ if hdr.ModTime.Unix() > 0 {
+ mTime = hdr.ModTime
+ }
+ if err := fs.Chtimes(path, aTime, mTime); err != nil {
+ return err
+ }
+ }
+
+ // Perform a final pass over extracted directories to update metadata
+ for _, dirHdr := range dirHdrs {
+ path := filepath.Join(dst, dirHdr.Name) // nolint:gosec // hdr.Name is checked above
+ // Chmod the directory since they might be created before we know the mode flags
+ if err := fs.Chmod(path, mode(dirHdr.FileInfo().Mode(), umask)); err != nil {
+ return err
+ }
+ // Set the mtime/atime attributes since they would have been changed during extraction
+ aTime := now
+ mTime := now
+ if dirHdr.AccessTime.Unix() > 0 {
+ aTime = dirHdr.AccessTime
+ }
+ if dirHdr.ModTime.Unix() > 0 {
+ mTime = dirHdr.ModTime
+ }
+ if err := fs.Chtimes(path, aTime, mTime); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// containsDotDot checks if the filepath value v contains a ".." entry.
+// This will check filepath components by splitting along / or \. This
+// function is copied directly from the Go net/http implementation.
+func containsDotDot(v string) bool {
+ if !strings.Contains(v, "..") {
+ return false
+ }
+ for _, ent := range strings.FieldsFunc(v, isSlashRune) {
+ if ent == ".." {
+ return true
+ }
+ }
+ return false
+}
+
+func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
diff --git a/internal/file/tar_xz_decompressor_test.go b/internal/file/tar_xz_decompressor_test.go
new file mode 100644
index 00000000000..69e863a3c34
--- /dev/null
+++ b/internal/file/tar_xz_decompressor_test.go
@@ -0,0 +1,207 @@
+package file
+
+import (
+ "archive/tar"
+ "bytes"
+ "path/filepath"
+ "testing"
+
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/ulikunitz/xz"
+)
+
+func TestTarXzDecompressor_Decompress(t *testing.T) {
+ files := map[string]string{
+ "file1.txt": "This is file 1.",
+ "file2.txt": "This is file 2.",
+ }
+
+ fs := afero.NewMemMapFs()
+ srcFile, tmpDir := createTarXzFromFiles(t, fs, files)
+ dstDir := filepath.Join(tmpDir, "decompressed")
+
+ decompressor := &tarXzDecompressor{
+ Fs: fs,
+ }
+
+ err := decompressor.Decompress(dstDir, srcFile, true, 0000)
+ require.NoError(t, err)
+
+ for name, content := range files {
+ data, err := afero.ReadFile(fs, filepath.Join(dstDir, name))
+ require.NoError(t, err)
+ assert.Equal(t, content, string(data))
+ }
+}
+
+func TestTarXzDecompressor_DecompressWithNestedDirs(t *testing.T) {
+ files := map[string]string{
+ "file1.txt": "This is file 1.",
+ "dir1/file2.txt": "This is file 2 in dir1.",
+ "dir1/dir2/file3.txt": "This is file 3 in dir1/dir2.",
+ "dir1/dir2/dir3/file4.txt": "This is file 4 in dir1/dir2/dir3.",
+ }
+
+ fs := afero.NewMemMapFs()
+ srcFile, tmpDir := createTarXzFromFiles(t, fs, files)
+ dstDir := filepath.Join(tmpDir, "decompressed")
+
+ decompressor := &tarXzDecompressor{
+ Fs: fs,
+ }
+
+ err := decompressor.Decompress(dstDir, srcFile, true, 0000)
+ require.NoError(t, err)
+
+ for name, content := range files {
+ data, err := afero.ReadFile(fs, filepath.Join(dstDir, name))
+ require.NoError(t, err)
+ assert.Equal(t, content, string(data))
+ }
+}
+
+func TestTarXzDecompressor_FileSizeLimit(t *testing.T) {
+ files := map[string]string{
+ "file1.txt": "This is file 1.",
+ "file2.txt": "This is file 2.",
+ }
+
+ fs := afero.NewMemMapFs()
+ srcFile, tmpDir := createTarXzFromFiles(t, fs, files)
+ dstDir := filepath.Join(tmpDir, "decompressed")
+
+ decompressor := &tarXzDecompressor{
+ FileSizeLimit: int64(10), // setting a small file size limit
+ Fs: fs,
+ }
+
+ err := decompressor.Decompress(dstDir, srcFile, true, 0000)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "tar archive larger than limit")
+}
+
+func TestTarXzDecompressor_FilesLimit(t *testing.T) {
+ files := map[string]string{
+ "file1.txt": "This is file 1.",
+ "file2.txt": "This is file 2.",
+ }
+
+ fs := afero.NewMemMapFs()
+ srcFile, tmpDir := createTarXzFromFiles(t, fs, files)
+ dstDir := filepath.Join(tmpDir, "decompressed")
+
+ decompressor := &tarXzDecompressor{
+ FilesLimit: 1, // setting a limit of 1 file
+ Fs: fs,
+ }
+
+ err := decompressor.Decompress(dstDir, srcFile, true, 0000)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "tar archive contains too many files")
+}
+
+func TestTarXzDecompressor_DecompressSingleFile(t *testing.T) {
+ files := map[string]string{
+ "file1.txt": "This is file 1.",
+ }
+
+ fs := afero.NewMemMapFs()
+ srcFile, tmpDir := createTarXzFromFiles(t, fs, files)
+ dstFile := filepath.Join(tmpDir, "single_file.txt")
+
+ decompressor := &tarXzDecompressor{
+ Fs: fs,
+ }
+
+ err := decompressor.Decompress(dstFile, srcFile, false, 0000)
+ require.NoError(t, err)
+
+ data, err := afero.ReadFile(fs, dstFile)
+ require.NoError(t, err)
+ assert.Equal(t, files["file1.txt"], string(data))
+}
+
+func TestTarXzDecompressor_EmptyArchive(t *testing.T) {
+ files := map[string]string{}
+
+ fs := afero.NewMemMapFs()
+ srcFile, tmpDir := createTarXzFromFiles(t, fs, files)
+ dstDir := filepath.Join(tmpDir, "decompressed")
+
+ decompressor := &tarXzDecompressor{
+ Fs: fs,
+ }
+
+ err := decompressor.Decompress(dstDir, srcFile, true, 0000)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "empty archive")
+}
+
+func TestTarXzDecompressor_PathTraversal(t *testing.T) {
+ files := map[string]string{
+ "../traversal_file.txt": "This file should not be extracted.",
+ }
+
+ fs := afero.NewMemMapFs()
+ srcFile, tmpDir := createTarXzFromFiles(t, fs, files)
+ dstDir := filepath.Join(tmpDir, "decompressed")
+
+ decompressor := &tarXzDecompressor{
+ Fs: fs,
+ }
+
+ err := decompressor.Decompress(dstDir, srcFile, true, 0000)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "entry contains '..'")
+}
+
+func createTarXzFromFiles(t *testing.T, fs afero.Fs, files map[string]string) (string, string) {
+ t.Helper()
+
+ tmpDir, err := afero.TempDir(fs, "", "tar_xz_decompressor_test")
+ require.NoError(t, err)
+ srcFile := filepath.Join(tmpDir, "src_file.tar.xz")
+
+ var buf bytes.Buffer
+ xzWriter, err := xz.NewWriter(&buf)
+ require.NoError(t, err)
+
+ tarWriter := tar.NewWriter(xzWriter)
+
+ for name, content := range files {
+ dir := filepath.Dir(name)
+ if dir != "." {
+ hdr := &tar.Header{
+ Name: dir + "/",
+ Mode: 0755,
+ Typeflag: tar.TypeDir,
+ }
+ err := tarWriter.WriteHeader(hdr)
+ require.NoError(t, err)
+ }
+
+ hdr := &tar.Header{
+ Name: name,
+ Mode: 0600,
+ Size: int64(len(content)),
+ }
+ err := tarWriter.WriteHeader(hdr)
+ require.NoError(t, err)
+
+ _, err = tarWriter.Write([]byte(content))
+ require.NoError(t, err)
+ }
+
+ err = tarWriter.Close()
+ require.NoError(t, err)
+
+ err = xzWriter.Close()
+ require.NoError(t, err)
+
+ err = afero.WriteFile(fs, srcFile, buf.Bytes(), 0644)
+ require.NoError(t, err)
+
+ return srcFile, tmpDir
+}
diff --git a/internal/file/xz_decompressor.go b/internal/file/xz_decompressor.go
new file mode 100644
index 00000000000..c76df201e35
--- /dev/null
+++ b/internal/file/xz_decompressor.go
@@ -0,0 +1,82 @@
+package file
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/spf13/afero"
+ "github.com/xi2/xz"
+)
+
+// Note: this is a copy of the XzDecompressor from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress_xz.go
+// with the xz lib swapped out (for performance). A few adjustments were made:
+// - refactored to use afero filesystem abstraction
+// - fixed some linting issues
+
+// xzDecompressor is an implementation of Decompressor that can decompress xz files.
+type xzDecompressor struct {
+ // FileSizeLimit limits the size of a decompressed file.
+ //
+ // The zero value means no limit.
+ FileSizeLimit int64
+
+ Fs afero.Fs
+}
+
+func (d *xzDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
+ // Directory isn't supported at all
+ if dir {
+ return fmt.Errorf("xz-compressed files can only unarchive to a single file")
+ }
+
+ // If we're going into a directory we should make that first
+ if err := d.Fs.MkdirAll(filepath.Dir(dst), mode(0755, umask)); err != nil {
+ return err
+ }
+
+ // File first
+ f, err := d.Fs.Open(src)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ // xz compression is second
+ xzR, err := xz.NewReader(f, 0)
+ if err != nil {
+ return err
+ }
+
+ // Copy it out, potentially using a file size limit.
+ return copyReader(d.Fs, dst, xzR, 0622, umask, d.FileSizeLimit)
+}
+
+// copyReader copies from an io.Reader into a file, using umask to create the dst file
+func copyReader(fs afero.Fs, dst string, src io.Reader, fmode, umask os.FileMode, fileSizeLimit int64) error {
+ dstF, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fmode)
+ if err != nil {
+ return err
+ }
+ defer dstF.Close()
+
+ if fileSizeLimit > 0 {
+ src = io.LimitReader(src, fileSizeLimit)
+ }
+
+ _, err = io.Copy(dstF, src)
+ if err != nil {
+ return err
+ }
+
+ // Explicitly chmod; the process umask is unconditionally applied otherwise.
+ // We'll mask the mode with our own umask, but that may be different than
+ // the process umask
+ return fs.Chmod(dst, mode(fmode, umask))
+}
+
+// mode returns the file mode masked by the umask
+func mode(mode, umask os.FileMode) os.FileMode {
+ return mode & ^umask
+}
diff --git a/internal/file/xz_decompressor_test.go b/internal/file/xz_decompressor_test.go
new file mode 100644
index 00000000000..a696eff0598
--- /dev/null
+++ b/internal/file/xz_decompressor_test.go
@@ -0,0 +1,102 @@
+package file
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/ulikunitz/xz"
+)
+
+func TestXzDecompressor_Decompress(t *testing.T) {
+ content := "This is a test for xz decompression."
+
+ fs := afero.NewMemMapFs()
+ srcFile, tmpDir := createXZFromString(t, fs, content)
+ dstFile := filepath.Join(tmpDir, "dst_file.txt")
+
+ decompressor := &xzDecompressor{
+ Fs: fs,
+ }
+
+ err := decompressor.Decompress(dstFile, srcFile, false, 0000)
+ require.NoError(t, err)
+
+ data, err := afero.ReadFile(fs, dstFile)
+ require.NoError(t, err)
+ assert.Equal(t, content, string(data))
+}
+
+func TestXzDecompressor_FileSizeLimit(t *testing.T) {
+ content := "This is a test for xz decompression with file size limit."
+
+ fs := afero.NewMemMapFs()
+ srcFile, tmpDir := createXZFromString(t, fs, content)
+ dstFile := filepath.Join(tmpDir, "dst_file.txt")
+
+ fileSizeLimit := int64(10)
+
+ decompressor := &xzDecompressor{
+ FileSizeLimit: fileSizeLimit,
+ Fs: fs,
+ }
+
+ err := decompressor.Decompress(dstFile, srcFile, false, 0000)
+ require.NoError(t, err)
+
+ data, err := afero.ReadFile(fs, dstFile)
+ require.NoError(t, err)
+ assert.Equal(t, content[:fileSizeLimit], string(data))
+}
+
+func TestCopyReader(t *testing.T) {
+ content := "This is the content for testing copyReader."
+
+ fs := afero.NewMemMapFs()
+
+ tmpDir := t.TempDir()
+ srcFile := filepath.Join(tmpDir, "src_file.txt")
+ err := afero.WriteFile(fs, srcFile, []byte(content), 0644)
+ require.NoError(t, err)
+
+ srcF, err := fs.Open(srcFile)
+ require.NoError(t, err)
+ defer srcF.Close()
+
+ dstFile := filepath.Join(tmpDir, "dst_file.txt")
+
+ err = copyReader(fs, dstFile, srcF, 0644, 0000, 0)
+ require.NoError(t, err)
+
+ info, err := fs.Stat(dstFile)
+ require.NoError(t, err)
+ assert.Equal(t, os.FileMode(0644), info.Mode().Perm())
+
+ data, err := afero.ReadFile(fs, dstFile)
+ assert.NoError(t, err)
+ assert.Equal(t, content, string(data))
+}
+
+func createXZFromString(t *testing.T, fs afero.Fs, content string) (string, string) {
+ t.Helper()
+
+ tmpDir, err := afero.TempDir(fs, "", "xz_decompressor_test")
+ require.NoError(t, err)
+ srcFile := filepath.Join(tmpDir, "src_file.xz")
+
+ f, err := fs.Create(srcFile)
+ require.NoError(t, err)
+ defer f.Close()
+
+ xzW, err := xz.NewWriter(f)
+ require.NoError(t, err)
+ defer xzW.Close()
+
+ _, err = xzW.Write([]byte(content))
+ assert.NoError(t, err)
+
+ return srcFile, tmpDir
+}
diff --git a/internal/format/presenter.go b/internal/format/presenter.go
index e365eaee587..71892d005b3 100644
--- a/internal/format/presenter.go
+++ b/internal/format/presenter.go
@@ -15,6 +15,7 @@ import (
type PresentationConfig struct {
TemplateFilePath string
ShowSuppressed bool
+ Pretty bool
}
// GetPresenter retrieves a Presenter that matches a CLI option
diff --git a/internal/format/writer.go b/internal/format/writer.go
index feb8f4ecdca..03e65652ff4 100644
--- a/internal/format/writer.go
+++ b/internal/format/writer.go
@@ -5,12 +5,12 @@ import (
"fmt"
"io"
"os"
- "path"
+ "path/filepath"
"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"
@@ -138,7 +138,7 @@ func newMultiWriter(options ...scanResultWriterDescription) (_ *scanResultMultiW
})
default:
// create any missing subdirectories
- dir := path.Dir(option.Path)
+ dir := filepath.Dir(option.Path)
if dir != "" {
s, err := os.Stat(dir)
if err != nil {
diff --git a/internal/format/writer_test.go b/internal/format/writer_test.go
index 54c3f2a76de..3d0c1af1f79 100644
--- a/internal/format/writer_test.go
+++ b/internal/format/writer_test.go
@@ -172,11 +172,7 @@ func Test_newSBOMMultiWriter(t *testing.T) {
switch w := mw.writers[i].(type) {
case *scanResultStreamWriter:
assert.Equal(t, string(w.format), e.format)
- if e.file != "" {
- assert.NotNil(t, w.out)
- } else {
- assert.NotNil(t, w.out)
- }
+ assert.NotNil(t, w.out)
if e.file != "" {
assert.FileExists(t, tmp+e.file)
}
diff --git a/internal/log/errors.go b/internal/log/errors.go
new file mode 100644
index 00000000000..3ebb0873f65
--- /dev/null
+++ b/internal/log/errors.go
@@ -0,0 +1,14 @@
+package log
+
+import "io"
+
+func CloseAndLogError(closer io.Closer, location string) {
+ if closer == nil {
+ Debug("no closer provided when attempting to close: %v", location)
+ return
+ }
+ err := closer.Close()
+ if err != nil {
+ Debug("failed to close file: %v due to: %v", location, err)
+ }
+}
diff --git a/internal/log/log.go b/internal/log/log.go
index 62b8cda9cea..ff3bbb68564 100644
--- a/internal/log/log.go
+++ b/internal/log/log.go
@@ -1,14 +1,27 @@
+/*
+Package log contains the singleton object and helper functions for facilitating logging within the syft library.
+*/
package log
import (
"github.com/anchore/go-logger"
"github.com/anchore/go-logger/adapter/discard"
+ "github.com/anchore/go-logger/adapter/redact"
+ red "github.com/anchore/grype/internal/redact"
)
-// log is the singleton used to facilitate logging internally within syft
-var log logger.Logger = discard.New()
+// log is the singleton used to facilitate logging internally within
+var log = discard.New()
func Set(l logger.Logger) {
+ // though the application will automatically have a redaction logger, library consumers may not be doing this.
+ // for this reason we additionally ensure there is a redaction logger configured for any logger passed. The
+ // source of truth for redaction values is still in the internal redact package. If the passed logger is already
+ // redacted, then this is a no-op.
+ store := red.Get()
+ if store != nil {
+ l = redact.New(l, store)
+ }
log = l
}
diff --git a/internal/logger/logrus.go b/internal/logger/logrus.go
deleted file mode 100644
index 865e359497c..00000000000
--- a/internal/logger/logrus.go
+++ /dev/null
@@ -1,143 +0,0 @@
-package logger
-
-import (
- "fmt"
- "io"
- "io/fs"
- "os"
-
- "github.com/sirupsen/logrus"
- prefixed "github.com/x-cray/logrus-prefixed-formatter"
-)
-
-const defaultLogFilePermissions fs.FileMode = 0644
-
-type LogrusConfig struct {
- EnableConsole bool
- EnableFile bool
- Structured bool
- Level logrus.Level
- FileLocation string
-}
-
-type LogrusLogger struct {
- Config LogrusConfig
- Logger *logrus.Logger
- Output io.Writer
-}
-
-type LogrusNestedLogger struct {
- Logger *logrus.Entry
-}
-
-func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
- appLogger := logrus.New()
-
- var output io.Writer
- switch {
- case cfg.EnableConsole && cfg.EnableFile:
- logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, defaultLogFilePermissions)
- if err != nil {
- panic(fmt.Errorf("unable to setup log file: %w", err))
- }
- output = io.MultiWriter(os.Stderr, logFile)
- case cfg.EnableConsole:
- output = os.Stderr
- case cfg.EnableFile:
- logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, defaultLogFilePermissions)
- if err != nil {
- panic(fmt.Errorf("unable to setup log file: %w", err))
- }
- output = logFile
- default:
- output = io.Discard
- }
-
- appLogger.SetOutput(output)
- appLogger.SetLevel(cfg.Level)
-
- if cfg.Structured {
- appLogger.SetFormatter(&logrus.JSONFormatter{
- TimestampFormat: "2006-01-02 15:04:05",
- DisableTimestamp: false,
- DisableHTMLEscape: false,
- PrettyPrint: false,
- })
- } else {
- appLogger.SetFormatter(&prefixed.TextFormatter{
- TimestampFormat: "2006-01-02 15:04:05",
- ForceColors: true,
- ForceFormatting: true,
- })
- }
-
- return &LogrusLogger{
- Config: cfg,
- Logger: appLogger,
- Output: output,
- }
-}
-
-func (l *LogrusLogger) Debugf(format string, args ...interface{}) {
- l.Logger.Debugf(format, args...)
-}
-
-func (l *LogrusLogger) Infof(format string, args ...interface{}) {
- l.Logger.Infof(format, args...)
-}
-
-func (l *LogrusLogger) Warnf(format string, args ...interface{}) {
- l.Logger.Warnf(format, args...)
-}
-
-func (l *LogrusLogger) Errorf(format string, args ...interface{}) {
- l.Logger.Errorf(format, args...)
-}
-
-func (l *LogrusLogger) Debug(args ...interface{}) {
- l.Logger.Debug(args...)
-}
-
-func (l *LogrusLogger) Info(args ...interface{}) {
- l.Logger.Info(args...)
-}
-
-func (l *LogrusLogger) Warn(args ...interface{}) {
- l.Logger.Warn(args...)
-}
-
-func (l *LogrusLogger) Error(args ...interface{}) {
- l.Logger.Error(args...)
-}
-
-func (l *LogrusNestedLogger) Debugf(format string, args ...interface{}) {
- l.Logger.Debugf(format, args...)
-}
-
-func (l *LogrusNestedLogger) Infof(format string, args ...interface{}) {
- l.Logger.Infof(format, args...)
-}
-
-func (l *LogrusNestedLogger) Warnf(format string, args ...interface{}) {
- l.Logger.Warnf(format, args...)
-}
-
-func (l *LogrusNestedLogger) Errorf(format string, args ...interface{}) {
- l.Logger.Errorf(format, args...)
-}
-
-func (l *LogrusNestedLogger) Debug(args ...interface{}) {
- l.Logger.Debug(args...)
-}
-
-func (l *LogrusNestedLogger) Info(args ...interface{}) {
- l.Logger.Info(args...)
-}
-
-func (l *LogrusNestedLogger) Warn(args ...interface{}) {
- l.Logger.Warn(args...)
-}
-
-func (l *LogrusNestedLogger) Error(args ...interface{}) {
- l.Logger.Error(args...)
-}
diff --git a/internal/redact/redact.go b/internal/redact/redact.go
new file mode 100644
index 00000000000..3bb76e2694d
--- /dev/null
+++ b/internal/redact/redact.go
@@ -0,0 +1,36 @@
+package redact
+
+import "github.com/anchore/go-logger/adapter/redact"
+
+var store redact.Store
+
+func Set(s redact.Store) {
+ if store != nil {
+ // if someone is trying to set a redaction store and we already have one then something is wrong. The store
+ // that we're replacing might already have values in it, so we should never replace it.
+ panic("replace existing redaction store (probably unintentional)")
+ }
+ store = s
+}
+
+func Get() redact.Store {
+ return store
+}
+
+func Add(vs ...string) {
+ if store == nil {
+ // if someone is trying to add values that should never be output and we don't have a store then something is wrong.
+ // we should never accidentally output values that should be redacted, thus we panic here.
+ panic("cannot add redactions without a store")
+ }
+ store.Add(vs...)
+}
+
+func Apply(value string) string {
+ if store == nil {
+ // if someone is trying to add values that should never be output and we don't have a store then something is wrong.
+ // we should never accidentally output values that should be redacted, thus we panic here.
+ panic("cannot apply redactions without a store")
+ }
+ return store.RedactString(value)
+}
diff --git a/internal/regex_helpers.go b/internal/regex_helpers.go
new file mode 100644
index 00000000000..7130f21a89c
--- /dev/null
+++ b/internal/regex_helpers.go
@@ -0,0 +1,45 @@
+package internal
+
+import "regexp"
+
+// MatchNamedCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map.
+// This is only for the first match in the regex. Callers shouldn't be providing regexes with multiple capture groups with the same name.
+func MatchNamedCaptureGroups(regEx *regexp.Regexp, content string) map[string]string {
+ // note: we are looking across all matches and stopping on the first non-empty match. Why? Take the following example:
+ // input: "cool something to match against" pattern: `((?Pmatch) (?Pagainst))?`. Since the pattern is
+ // encapsulated in an optional capture group, there will be results for each character, but the results will match
+ // on nothing. The only "true" match will be at the end ("match against").
+ allMatches := regEx.FindAllStringSubmatch(content, -1)
+ var results map[string]string
+ for _, match := range allMatches {
+ // fill a candidate results map with named capture group results, accepting empty values, but not groups with
+ // no names
+ for nameIdx, name := range regEx.SubexpNames() {
+ if nameIdx > len(match) || len(name) == 0 {
+ continue
+ }
+ if results == nil {
+ results = make(map[string]string)
+ }
+ results[name] = match[nameIdx]
+ }
+ // note: since we are looking for the first best potential match we should stop when we find the first one
+ // with non-empty results.
+ if !isEmptyMap(results) {
+ break
+ }
+ }
+ return results
+}
+
+func isEmptyMap(m map[string]string) bool {
+ if len(m) == 0 {
+ return true
+ }
+ for _, value := range m {
+ if value != "" {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/regex_helpers_test.go b/internal/regex_helpers_test.go
new file mode 100644
index 00000000000..1c483775309
--- /dev/null
+++ b/internal/regex_helpers_test.go
@@ -0,0 +1,70 @@
+package internal
+
+import (
+ "regexp"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMatchCaptureGroups(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ pattern string
+ expected map[string]string
+ }{
+ {
+ name: "go-case",
+ input: "match this thing",
+ pattern: `(?Pmatch).*(?Pthing)`,
+ expected: map[string]string{
+ "name": "match",
+ "version": "thing",
+ },
+ },
+ {
+ name: "only matches the first instance",
+ input: "match this thing batch another think",
+ pattern: `(?P[mb]atch).*?(?Pthin[gk])`,
+ expected: map[string]string{
+ "name": "match",
+ "version": "thing",
+ },
+ },
+ {
+ name: "nested capture groups",
+ input: "cool something to match against",
+ pattern: `((?Pmatch) (?Pagainst))`,
+ expected: map[string]string{
+ "name": "match",
+ "version": "against",
+ },
+ },
+ {
+ name: "nested optional capture groups",
+ input: "cool something to match against",
+ pattern: `((?Pmatch) (?Pagainst))?`,
+ expected: map[string]string{
+ "name": "match",
+ "version": "against",
+ },
+ },
+ {
+ name: "nested optional capture groups with larger match",
+ input: "cool something to match against match never",
+ pattern: `.*?((?Pmatch) (?P(against|never)))?`,
+ expected: map[string]string{
+ "name": "match",
+ "version": "against",
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ actual := MatchNamedCaptureGroups(regexp.MustCompile(test.pattern), test.input)
+ assert.Equal(t, test.expected, actual)
+ })
+ }
+}
diff --git a/internal/schemaver/schema_ver.go b/internal/schemaver/schema_ver.go
new file mode 100644
index 00000000000..83423f3ef3c
--- /dev/null
+++ b/internal/schemaver/schema_ver.go
@@ -0,0 +1,88 @@
+package schemaver
+
+import (
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+type SchemaVer struct {
+ Model int // breaking changes
+ Revision int // potentially-breaking changes
+ Addition int // additions only
+}
+
+func New(model, revision, addition int) SchemaVer {
+ return SchemaVer{
+ Model: model,
+ Revision: revision,
+ Addition: addition,
+ }
+}
+
+func Parse(s string) (SchemaVer, error) {
+ // must provide model.revision.addition
+ parts := strings.Split(strings.TrimSpace(s), ".")
+ if len(parts) != 3 {
+ return SchemaVer{}, fmt.Errorf("invalid schema version format: %s", s)
+ }
+ // check that all parts are integers
+ var values [3]int
+ for i, part := range parts {
+ if i == 0 {
+ part = strings.TrimPrefix(part, "v")
+ }
+ v, err := strconv.Atoi(part)
+ if err != nil || v < 0 {
+ return SchemaVer{}, fmt.Errorf("invalid schema version format: %s", s)
+ }
+ values[i] = v
+ }
+ if values[0] < 1 {
+ return SchemaVer{}, fmt.Errorf("model value must be greater than 0: %s", s)
+ }
+ return New(values[0], values[1], values[2]), nil
+}
+
+func (s SchemaVer) Valid() bool {
+ return s.Model > 0 && s.Revision >= 0 && s.Addition >= 0
+}
+
+func (s SchemaVer) MarshalJSON() ([]byte, error) {
+ return []byte(fmt.Sprintf(`"%s"`, s.String())), nil
+}
+
+func (s *SchemaVer) UnmarshalJSON(data []byte) error {
+ var str string
+ if err := json.Unmarshal(data, &str); err != nil {
+ return fmt.Errorf("failed to unmarshal schema version as string: %w", err)
+ }
+
+ parsed, err := Parse(str)
+ if err != nil {
+ return fmt.Errorf("failed to parse schema version: %w", err)
+ }
+ *s = parsed
+ return nil
+}
+
+func (s SchemaVer) String() string {
+ return fmt.Sprintf("v%d.%d.%d", s.Model, s.Revision, s.Addition)
+}
+
+func (s SchemaVer) LessThan(other SchemaVer) bool {
+ if s.Model != other.Model {
+ return s.Model < other.Model
+ }
+
+ if s.Revision != other.Revision {
+ return s.Revision < other.Revision
+ }
+
+ return s.Addition < other.Addition
+}
+
+func (s SchemaVer) GreaterOrEqualTo(other SchemaVer) bool {
+ return !s.LessThan(other)
+}
diff --git a/internal/schemaver/schema_ver_test.go b/internal/schemaver/schema_ver_test.go
new file mode 100644
index 00000000000..04d5bf84d6d
--- /dev/null
+++ b/internal/schemaver/schema_ver_test.go
@@ -0,0 +1,245 @@
+package schemaver
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSchemaVerComparisons(t *testing.T) {
+ tests := []struct {
+ name string
+ v1 SchemaVer
+ v2 SchemaVer
+ lessThan bool
+ greaterOrEqual bool
+ }{
+ {
+ name: "equal versions",
+ v1: New(1, 0, 0),
+ v2: New(1, 0, 0),
+ lessThan: false,
+ greaterOrEqual: true,
+ },
+ {
+ name: "different model versions",
+ v1: New(1, 0, 0),
+ v2: New(2, 0, 0),
+ lessThan: true,
+ greaterOrEqual: false,
+ },
+ {
+ name: "different revision versions",
+ v1: New(1, 1, 0),
+ v2: New(1, 2, 0),
+ lessThan: true,
+ greaterOrEqual: false,
+ },
+ {
+ name: "different addition versions",
+ v1: New(1, 0, 1),
+ v2: New(1, 0, 2),
+ lessThan: true,
+ greaterOrEqual: false,
+ },
+ {
+ name: "inverted addition versions",
+ v1: New(1, 0, 2),
+ v2: New(1, 0, 1),
+ lessThan: false,
+ greaterOrEqual: true,
+ },
+ {
+ name: "greater model overrides lower revision",
+ v1: New(2, 0, 0),
+ v2: New(1, 9, 9),
+ lessThan: false,
+ greaterOrEqual: true,
+ },
+ {
+ name: "greater revision overrides lower addition",
+ v1: New(1, 2, 0),
+ v2: New(1, 1, 9),
+ lessThan: false,
+ greaterOrEqual: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.v1.LessThan(tt.v2); got != tt.lessThan {
+ t.Errorf("LessThan() = %v, want %v", got, tt.lessThan)
+ }
+ if got := tt.v1.GreaterOrEqualTo(tt.v2); got != tt.greaterOrEqual {
+ t.Errorf("GreaterOrEqualTo() = %v, want %v", got, tt.greaterOrEqual)
+ }
+ })
+ }
+}
+
+func TestParse(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want SchemaVer
+ wantErr bool
+ }{
+ {
+ name: "valid version",
+ input: "1.2.3",
+ want: New(1, 2, 3),
+ wantErr: false,
+ },
+ {
+ name: "valid large numbers",
+ input: "999.888.777",
+ want: New(999, 888, 777),
+ wantErr: false,
+ },
+ {
+ name: "valid with whitespace",
+ input: " 1.2.3 ",
+ want: New(1, 2, 3),
+ wantErr: false,
+ },
+ {
+ name: "invalid version with zeros",
+ input: "0.0.0",
+ want: New(0, 0, 0),
+ wantErr: true,
+ },
+ {
+ name: "invalid empty string",
+ input: "",
+ wantErr: true,
+ },
+ {
+ name: "invalid too few parts",
+ input: "1.2",
+ wantErr: true,
+ },
+ {
+ name: "invalid too many parts",
+ input: "1.2.3.4",
+ wantErr: true,
+ },
+ {
+ name: "invalid non-numeric model",
+ input: "a.2.3",
+ wantErr: true,
+ },
+ {
+ name: "invalid non-numeric revision",
+ input: "1.b.3",
+ wantErr: true,
+ },
+ {
+ name: "invalid non-numeric addition",
+ input: "1.2.c",
+ wantErr: true,
+ },
+ {
+ name: "invalid negative number",
+ input: "-1.2.3",
+ wantErr: true,
+ },
+ {
+ name: "invalid format with spaces",
+ input: "1 . 2 . 3",
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := Parse(tt.input)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr && (got.Model != tt.want.Model ||
+ got.Revision != tt.want.Revision ||
+ got.Addition != tt.want.Addition) {
+ t.Errorf("Parse() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSchemaVer_Valid(t *testing.T) {
+ tests := []struct {
+ name string
+ schema SchemaVer
+ expected bool
+ }{
+ {
+ name: "valid schema version - all positive",
+ schema: SchemaVer{
+ Model: 1,
+ Revision: 1,
+ Addition: 1,
+ },
+ expected: true,
+ },
+ {
+ name: "valid schema version - zero revision and addition",
+ schema: SchemaVer{
+ Model: 1,
+ Revision: 0,
+ Addition: 0,
+ },
+ expected: true,
+ },
+ {
+ name: "invalid - zero model",
+ schema: SchemaVer{
+ Model: 0,
+ Revision: 1,
+ Addition: 1,
+ },
+ expected: false,
+ },
+ {
+ name: "invalid - negative model",
+ schema: SchemaVer{
+ Model: -1,
+ Revision: 1,
+ Addition: 1,
+ },
+ expected: false,
+ },
+ {
+ name: "invalid - negative revision",
+ schema: SchemaVer{
+ Model: 1,
+ Revision: -1,
+ Addition: 1,
+ },
+ expected: false,
+ },
+ {
+ name: "invalid - negative addition",
+ schema: SchemaVer{
+ Model: 1,
+ Revision: 1,
+ Addition: -1,
+ },
+ expected: false,
+ },
+ {
+ name: "invalid - all negative",
+ schema: SchemaVer{
+ Model: -1,
+ Revision: -1,
+ Addition: -1,
+ },
+ expected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expected, tt.schema.Valid())
+ })
+ }
+}
diff --git a/internal/stringutil/string_helpers.go b/internal/stringutil/string_helpers.go
index 1ff56e35c54..25d21f02c5c 100644
--- a/internal/stringutil/string_helpers.go
+++ b/internal/stringutil/string_helpers.go
@@ -23,3 +23,14 @@ func HasAnyOfPrefixes(input string, prefixes ...string) bool {
return false
}
+
+// SplitCommaSeparatedString returns a slice of strings separated from the input string by commas
+func SplitCommaSeparatedString(input string) []string {
+ output := make([]string, 0)
+ for _, inputItem := range strings.Split(input, ",") {
+ if len(inputItem) > 0 {
+ output = append(output, inputItem)
+ }
+ }
+ return output
+}
diff --git a/internal/stringutil/string_helpers_test.go b/internal/stringutil/string_helpers_test.go
index b5171686801..89baa28f9b1 100644
--- a/internal/stringutil/string_helpers_test.go
+++ b/internal/stringutil/string_helpers_test.go
@@ -120,3 +120,37 @@ func TestHasAnyOfPrefixes(t *testing.T) {
})
}
}
+
+func TestSplitCommaSeparatedString(t *testing.T) {
+ tests := []struct {
+ input string
+ expected []string
+ }{
+ {
+ input: "testing",
+ expected: []string{"testing"},
+ },
+ {
+ input: "",
+ expected: []string{},
+ },
+ {
+ input: "testing1,testing2",
+ expected: []string{"testing1", "testing2"},
+ },
+ {
+ input: "testing1,,testing2,testing3",
+ expected: []string{"testing1", "testing2", "testing3"},
+ },
+ {
+ input: "testing1,testing2,,",
+ expected: []string{"testing1", "testing2"},
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.input, func(t *testing.T) {
+ assert.Equal(t, test.expected, SplitCommaSeparatedString(test.input))
+ })
+ }
+}
diff --git a/internal/version/build.go b/internal/version/build.go
deleted file mode 100644
index c8f20caa696..00000000000
--- a/internal/version/build.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package version
-
-import (
- "fmt"
- "runtime"
- "runtime/debug"
- "strings"
-
- "github.com/anchore/grype/internal/log"
-)
-
-const valueNotProvided = "[not provided]"
-
-// all variables here are provided as build-time arguments, with clear default values
-var version = valueNotProvided
-var gitCommit = valueNotProvided
-var gitDescription = valueNotProvided
-var buildDate = valueNotProvided
-var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
-
-// Version defines the application version details (generally from build information)
-type Version struct {
- Version string `json:"version"` // application semantic version
- SyftVersion string `json:"syftVersion"` // the version of syft being used by grype
- GitCommit string `json:"gitCommit"` // git SHA at build-time
- GitDescription string `json:"gitDescription"` // output of 'git describe --dirty --always --tags'
- BuildDate string `json:"buildDate"` // date of the build
- GoVersion string `json:"goVersion"` // go runtime version at build-time
- Compiler string `json:"compiler"` // compiler used at build-time
- Platform string `json:"platform"` // GOOS and GOARCH at build-time
-}
-
-func (v Version) isProductionBuild() bool {
- if strings.Contains(v.Version, "SNAPSHOT") || strings.Contains(v.Version, valueNotProvided) {
- return false
- }
- return true
-}
-
-// FromBuild provides all version details
-func FromBuild() Version {
- actualSyftVersion, err := extractSyftVersion()
- if err != nil {
- // TODO: parameterize error
- log.Trace("unable to find syft version")
- actualSyftVersion = valueNotProvided
- }
- return Version{
- Version: version,
- SyftVersion: actualSyftVersion,
- GitCommit: gitCommit,
- GitDescription: gitDescription,
- BuildDate: buildDate,
- GoVersion: runtime.Version(),
- Compiler: runtime.Compiler,
- Platform: platform,
- }
-}
-
-func extractSyftVersion() (string, error) {
- buildInfo, ok := debug.ReadBuildInfo()
- if !ok {
- return "", fmt.Errorf("unable to find the buildinfo section of the binary (syft version is unknown)")
- }
-
- for _, d := range buildInfo.Deps {
- if d.Path == "github.com/anchore/syft" {
- return d.Version, nil
- }
- }
-
- return "", fmt.Errorf("unable to find 'github.com/anchore/syft' from the buildinfo section of the binary")
-}
diff --git a/schema/cyclonedx/.gitignore b/schema/cyclonedx/.gitignore
deleted file mode 100644
index 472d439fa14..00000000000
--- a/schema/cyclonedx/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-bom.xml
-bom.json
diff --git a/schema/cyclonedx/Makefile b/schema/cyclonedx/Makefile
deleted file mode 100644
index 7242a959669..00000000000
--- a/schema/cyclonedx/Makefile
+++ /dev/null
@@ -1,14 +0,0 @@
-.DEFAULT_GOAL := validate-schema
-
-.PHONY: validate-schema
-validate-schema: validate-schema-xml validate-schema-json
-
-.PHONY: validate-schema-xml
-validate-schema-xml:
- go run ../../cmd/grype -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o cyclonedx-xml > bom.xml
- xmllint --noout --schema ./cyclonedx.xsd bom.xml
-
-.PHONY: validate-schema-json
-validate-schema-json:
- go run ../../cmd/grype -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o cyclonedx-json > bom.json
- ../../.tmp/yajsv -s cyclonedx.json bom.json
diff --git a/schema/cyclonedx/README.md b/schema/cyclonedx/README.md
deleted file mode 100644
index beed2007fdd..00000000000
--- a/schema/cyclonedx/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# CycloneDX Schemas
-
-`grype` generates a CycloneDX output. This validation is similar to what is done in `syft`, validating output against CycloneDX schemas.
-
-Validation is done with `xmllint`, which requires a copy of all schemas because it can't work with HTTP references. The schemas are modified to reference local copies of dependent schemas.
diff --git a/schema/cyclonedx/cyclonedx.json b/schema/cyclonedx/cyclonedx.json
deleted file mode 100644
index 627cbc7dba4..00000000000
--- a/schema/cyclonedx/cyclonedx.json
+++ /dev/null
@@ -1,1697 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "http://cyclonedx.org/schema/bom-1.4.schema.json",
- "type": "object",
- "title": "CycloneDX Software Bill of Materials Standard",
- "$comment" : "CycloneDX JSON schema is published under the terms of the Apache License 2.0.",
- "required": [
- "bomFormat",
- "specVersion",
- "version"
- ],
- "additionalProperties": false,
- "properties": {
- "$schema": {
- "type": "string",
- "enum": [
- "http://cyclonedx.org/schema/bom-1.4.schema.json"
- ]
- },
- "bomFormat": {
- "type": "string",
- "title": "BOM Format",
- "description": "Specifies the format of the BOM. This helps to identify the file as CycloneDX since BOMs do not have a filename convention nor does JSON schema support namespaces. This value MUST be \"CycloneDX\".",
- "enum": [
- "CycloneDX"
- ]
- },
- "specVersion": {
- "type": "string",
- "title": "CycloneDX Specification Version",
- "description": "The version of the CycloneDX specification a BOM conforms to (starting at version 1.2).",
- "examples": ["1.4"]
- },
- "serialNumber": {
- "type": "string",
- "title": "BOM Serial Number",
- "description": "Every BOM generated SHOULD have a unique serial number, even if the contents of the BOM have not changed over time. If specified, the serial number MUST conform to RFC-4122. Use of serial numbers are RECOMMENDED.",
- "examples": ["urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"],
- "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
- },
- "version": {
- "type": "integer",
- "title": "BOM Version",
- "description": "Whenever an existing BOM is modified, either manually or through automated processes, the version of the BOM SHOULD be incremented by 1. When a system is presented with multiple BOMs with identical serial numbers, the system SHOULD use the most recent version of the BOM. The default version is '1'.",
- "default": 1,
- "examples": [1]
- },
- "metadata": {
- "$ref": "#/definitions/metadata",
- "title": "BOM Metadata",
- "description": "Provides additional information about a BOM."
- },
- "components": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/component"},
- "uniqueItems": true,
- "title": "Components",
- "description": "A list of software and hardware components."
- },
- "services": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/service"},
- "uniqueItems": true,
- "title": "Services",
- "description": "A list of services. This may include microservices, function-as-a-service, and other types of network or intra-process services."
- },
- "externalReferences": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/externalReference"},
- "title": "External References",
- "description": "External references provide a way to document systems, sites, and information that may be relevant but which are not included with the BOM."
- },
- "dependencies": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/dependency"},
- "uniqueItems": true,
- "title": "Dependencies",
- "description": "Provides the ability to document dependency relationships."
- },
- "compositions": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/compositions"},
- "uniqueItems": true,
- "title": "Compositions",
- "description": "Compositions describe constituent parts (including components, services, and dependency relationships) and their completeness."
- },
- "vulnerabilities": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/vulnerability"},
- "uniqueItems": true,
- "title": "Vulnerabilities",
- "description": "Vulnerabilities identified in components or services."
- },
- "signature": {
- "$ref": "#/definitions/signature",
- "title": "Signature",
- "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)."
- }
- },
- "definitions": {
- "refType": {
- "$comment": "Identifier-DataType for interlinked elements.",
- "type": "string"
- },
- "metadata": {
- "type": "object",
- "title": "BOM Metadata Object",
- "additionalProperties": false,
- "properties": {
- "timestamp": {
- "type": "string",
- "format": "date-time",
- "title": "Timestamp",
- "description": "The date and time (timestamp) when the BOM was created."
- },
- "tools": {
- "type": "array",
- "title": "Creation Tools",
- "description": "The tool(s) used in the creation of the BOM.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/tool"}
- },
- "authors" :{
- "type": "array",
- "title": "Authors",
- "description": "The person(s) who created the BOM. Authors are common in BOMs created through manual processes. BOMs created through automated means may not have authors.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/organizationalContact"}
- },
- "component": {
- "title": "Component",
- "description": "The component that the BOM describes.",
- "$ref": "#/definitions/component"
- },
- "manufacture": {
- "title": "Manufacture",
- "description": "The organization that manufactured the component that the BOM describes.",
- "$ref": "#/definitions/organizationalEntity"
- },
- "supplier": {
- "title": "Supplier",
- "description": " The organization that supplied the component that the BOM describes. The supplier may often be the manufacturer, but may also be a distributor or repackager.",
- "$ref": "#/definitions/organizationalEntity"
- },
- "licenses": {
- "type": "array",
- "title": "BOM License(s)",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/licenseChoice"}
- },
- "properties": {
- "type": "array",
- "title": "Properties",
- "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/property"}
- }
- }
- },
- "tool": {
- "type": "object",
- "title": "Tool",
- "description": "Information about the automated or manual tool used",
- "additionalProperties": false,
- "properties": {
- "vendor": {
- "type": "string",
- "title": "Tool Vendor",
- "description": "The name of the vendor who created the tool"
- },
- "name": {
- "type": "string",
- "title": "Tool Name",
- "description": "The name of the tool"
- },
- "version": {
- "type": "string",
- "title": "Tool Version",
- "description": "The version of the tool"
- },
- "hashes": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/hash"},
- "title": "Hashes",
- "description": "The hashes of the tool (if applicable)."
- },
- "externalReferences": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/externalReference"},
- "title": "External References",
- "description": "External references provide a way to document systems, sites, and information that may be relevant but which are not included with the BOM."
- }
- }
- },
- "organizationalEntity": {
- "type": "object",
- "title": "Organizational Entity Object",
- "description": "",
- "additionalProperties": false,
- "properties": {
- "name": {
- "type": "string",
- "title": "Name",
- "description": "The name of the organization",
- "examples": [
- "Example Inc."
- ]
- },
- "url": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "iri-reference"
- },
- "title": "URL",
- "description": "The URL of the organization. Multiple URLs are allowed.",
- "examples": ["https://example.com"]
- },
- "contact": {
- "type": "array",
- "title": "Contact",
- "description": "A contact at the organization. Multiple contacts are allowed.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/organizationalContact"}
- }
- }
- },
- "organizationalContact": {
- "type": "object",
- "title": "Organizational Contact Object",
- "description": "",
- "additionalProperties": false,
- "properties": {
- "name": {
- "type": "string",
- "title": "Name",
- "description": "The name of a contact",
- "examples": ["Contact name"]
- },
- "email": {
- "type": "string",
- "format": "idn-email",
- "title": "Email Address",
- "description": "The email address of the contact.",
- "examples": ["firstname.lastname@example.com"]
- },
- "phone": {
- "type": "string",
- "title": "Phone",
- "description": "The phone number of the contact.",
- "examples": ["800-555-1212"]
- }
- }
- },
- "component": {
- "type": "object",
- "title": "Component Object",
- "required": [
- "type",
- "name"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "type": "string",
- "enum": [
- "application",
- "framework",
- "library",
- "container",
- "operating-system",
- "device",
- "firmware",
- "file"
- ],
- "title": "Component Type",
- "description": "Specifies the type of component. For software components, classify as application if no more specific appropriate classification is available or cannot be determined for the component. Types include:\n\n* __application__ = A software application. Refer to [https://en.wikipedia.org/wiki/Application_software](https://en.wikipedia.org/wiki/Application_software) for information about applications.\n* __framework__ = A software framework. Refer to [https://en.wikipedia.org/wiki/Software_framework](https://en.wikipedia.org/wiki/Software_framework) for information on how frameworks vary slightly from libraries.\n* __library__ = A software library. Refer to [https://en.wikipedia.org/wiki/Library_(computing)](https://en.wikipedia.org/wiki/Library_(computing))\n for information about libraries. All third-party and open source reusable components will likely be a library. If the library also has key features of a framework, then it should be classified as a framework. If not, or is unknown, then specifying library is RECOMMENDED.\n* __container__ = A packaging and/or runtime format, not specific to any particular technology, which isolates software inside the container from software outside of a container through virtualization technology. Refer to [https://en.wikipedia.org/wiki/OS-level_virtualization](https://en.wikipedia.org/wiki/OS-level_virtualization)\n* __operating-system__ = A software operating system without regard to deployment model (i.e. installed on physical hardware, virtual machine, image, etc) Refer to [https://en.wikipedia.org/wiki/Operating_system](https://en.wikipedia.org/wiki/Operating_system)\n* __device__ = A hardware device such as a processor, or chip-set. A hardware device containing firmware SHOULD include a component for the physical hardware itself, and another component of type 'firmware' or 'operating-system' (whichever is relevant), describing information about the software running on the device.\n* __firmware__ = A special type of software that provides low-level control over a devices hardware. Refer to [https://en.wikipedia.org/wiki/Firmware](https://en.wikipedia.org/wiki/Firmware)\n* __file__ = A computer file. Refer to [https://en.wikipedia.org/wiki/Computer_file](https://en.wikipedia.org/wiki/Computer_file) for information about files.",
- "examples": ["library"]
- },
- "mime-type": {
- "type": "string",
- "title": "Mime-Type",
- "description": "The optional mime-type of the component. When used on file components, the mime-type can provide additional context about the kind of file being represented such as an image, font, or executable. Some library or framework components may also have an associated mime-type.",
- "examples": ["image/jpeg"],
- "pattern": "^[-+a-z0-9.]+/[-+a-z0-9.]+$"
- },
- "bom-ref": {
- "$ref": "#/definitions/refType",
- "title": "BOM Reference",
- "description": "An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be unique within the BOM."
- },
- "supplier": {
- "title": "Component Supplier",
- "description": " The organization that supplied the component. The supplier may often be the manufacturer, but may also be a distributor or repackager.",
- "$ref": "#/definitions/organizationalEntity"
- },
- "author": {
- "type": "string",
- "title": "Component Author",
- "description": "The person(s) or organization(s) that authored the component",
- "examples": ["Acme Inc"]
- },
- "publisher": {
- "type": "string",
- "title": "Component Publisher",
- "description": "The person(s) or organization(s) that published the component",
- "examples": ["Acme Inc"]
- },
- "group": {
- "type": "string",
- "title": "Component Group",
- "description": "The grouping name or identifier. This will often be a shortened, single name of the company or project that produced the component, or the source package or domain name. Whitespace and special characters should be avoided. Examples include: apache, org.apache.commons, and apache.org.",
- "examples": ["com.acme"]
- },
- "name": {
- "type": "string",
- "title": "Component Name",
- "description": "The name of the component. This will often be a shortened, single name of the component. Examples: commons-lang3 and jquery",
- "examples": ["tomcat-catalina"]
- },
- "version": {
- "type": "string",
- "title": "Component Version",
- "description": "The component version. The version should ideally comply with semantic versioning but is not enforced.",
- "examples": ["9.0.14"]
- },
- "description": {
- "type": "string",
- "title": "Component Description",
- "description": "Specifies a description for the component"
- },
- "scope": {
- "type": "string",
- "enum": [
- "required",
- "optional",
- "excluded"
- ],
- "title": "Component Scope",
- "description": "Specifies the scope of the component. If scope is not specified, 'required' scope SHOULD be assumed by the consumer of the BOM.",
- "default": "required"
- },
- "hashes": {
- "type": "array",
- "title": "Component Hashes",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/hash"}
- },
- "licenses": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/licenseChoice"},
- "title": "Component License(s)"
- },
- "copyright": {
- "type": "string",
- "title": "Component Copyright",
- "description": "A copyright notice informing users of the underlying claims to copyright ownership in a published work.",
- "examples": ["Acme Inc"]
- },
- "cpe": {
- "type": "string",
- "title": "Component Common Platform Enumeration (CPE)",
- "description": "Specifies a well-formed CPE name that conforms to the CPE 2.2 or 2.3 specification. See [https://nvd.nist.gov/products/cpe](https://nvd.nist.gov/products/cpe)",
- "examples": ["cpe:2.3:a:acme:component_framework:-:*:*:*:*:*:*:*"]
- },
- "purl": {
- "type": "string",
- "title": "Component Package URL (purl)",
- "description": "Specifies the package-url (purl). The purl, if specified, MUST be valid and conform to the specification defined at: [https://github.com/package-url/purl-spec](https://github.com/package-url/purl-spec)",
- "examples": ["pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar"]
- },
- "swid": {
- "$ref": "#/definitions/swid",
- "title": "SWID Tag",
- "description": "Specifies metadata and content for [ISO-IEC 19770-2 Software Identification (SWID) Tags](https://www.iso.org/standard/65666.html)."
- },
- "modified": {
- "type": "boolean",
- "title": "Component Modified From Original",
- "description": "[Deprecated] - DO NOT USE. This will be removed in a future version. Use the pedigree element instead to supply information on exactly how the component was modified. A boolean value indicating if the component has been modified from the original. A value of true indicates the component is a derivative of the original. A value of false indicates the component has not been modified from the original."
- },
- "pedigree": {
- "type": "object",
- "title": "Component Pedigree",
- "description": "Component pedigree is a way to document complex supply chain scenarios where components are created, distributed, modified, redistributed, combined with other components, etc. Pedigree supports viewing this complex chain from the beginning, the end, or anywhere in the middle. It also provides a way to document variants where the exact relation may not be known.",
- "additionalProperties": false,
- "properties": {
- "ancestors": {
- "type": "array",
- "title": "Ancestors",
- "description": "Describes zero or more components in which a component is derived from. This is commonly used to describe forks from existing projects where the forked version contains a ancestor node containing the original component it was forked from. For example, Component A is the original component. Component B is the component being used and documented in the BOM. However, Component B contains a pedigree node with a single ancestor documenting Component A - the original component from which Component B is derived from.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/component"}
- },
- "descendants": {
- "type": "array",
- "title": "Descendants",
- "description": "Descendants are the exact opposite of ancestors. This provides a way to document all forks (and their forks) of an original or root component.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/component"}
- },
- "variants": {
- "type": "array",
- "title": "Variants",
- "description": "Variants describe relations where the relationship between the components are not known. For example, if Component A contains nearly identical code to Component B. They are both related, but it is unclear if one is derived from the other, or if they share a common ancestor.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/component"}
- },
- "commits": {
- "type": "array",
- "title": "Commits",
- "description": "A list of zero or more commits which provide a trail describing how the component deviates from an ancestor, descendant, or variant.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/commit"}
- },
- "patches": {
- "type": "array",
- "title": "Patches",
- "description": ">A list of zero or more patches describing how the component deviates from an ancestor, descendant, or variant. Patches may be complimentary to commits or may be used in place of commits.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/patch"}
- },
- "notes": {
- "type": "string",
- "title": "Notes",
- "description": "Notes, observations, and other non-structured commentary describing the components pedigree."
- }
- }
- },
- "externalReferences": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/externalReference"},
- "title": "External References",
- "description": "External references provide a way to document systems, sites, and information that may be relevant but which are not included with the BOM."
- },
- "components": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/component"},
- "uniqueItems": true,
- "title": "Components",
- "description": "A list of software and hardware components included in the parent component. This is not a dependency tree. It provides a way to specify a hierarchical representation of component assemblies, similar to system → subsystem → parts assembly in physical supply chains."
- },
- "evidence": {
- "$ref": "#/definitions/componentEvidence",
- "title": "Evidence",
- "description": "Provides the ability to document evidence collected through various forms of extraction or analysis."
- },
- "releaseNotes": {
- "$ref": "#/definitions/releaseNotes",
- "title": "Release notes",
- "description": "Specifies optional release notes."
- },
- "properties": {
- "type": "array",
- "title": "Properties",
- "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/property"}
- },
- "signature": {
- "$ref": "#/definitions/signature",
- "title": "Signature",
- "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)."
- }
- }
- },
- "swid": {
- "type": "object",
- "title": "SWID Tag",
- "description": "Specifies metadata and content for ISO-IEC 19770-2 Software Identification (SWID) Tags.",
- "required": [
- "tagId",
- "name"
- ],
- "additionalProperties": false,
- "properties": {
- "tagId": {
- "type": "string",
- "title": "Tag ID",
- "description": "Maps to the tagId of a SoftwareIdentity."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Maps to the name of a SoftwareIdentity."
- },
- "version": {
- "type": "string",
- "title": "Version",
- "default": "0.0",
- "description": "Maps to the version of a SoftwareIdentity."
- },
- "tagVersion": {
- "type": "integer",
- "title": "Tag Version",
- "default": 0,
- "description": "Maps to the tagVersion of a SoftwareIdentity."
- },
- "patch": {
- "type": "boolean",
- "title": "Patch",
- "default": false,
- "description": "Maps to the patch of a SoftwareIdentity."
- },
- "text": {
- "title": "Attachment text",
- "description": "Specifies the metadata and content of the SWID tag.",
- "$ref": "#/definitions/attachment"
- },
- "url": {
- "type": "string",
- "title": "URL",
- "description": "The URL to the SWID file.",
- "format": "iri-reference"
- }
- }
- },
- "attachment": {
- "type": "object",
- "title": "Attachment",
- "description": "Specifies the metadata and content for an attachment.",
- "required": [
- "content"
- ],
- "additionalProperties": false,
- "properties": {
- "contentType": {
- "type": "string",
- "title": "Content-Type",
- "description": "Specifies the content type of the text. Defaults to text/plain if not specified.",
- "default": "text/plain"
- },
- "encoding": {
- "type": "string",
- "title": "Encoding",
- "description": "Specifies the optional encoding the text is represented in.",
- "enum": [
- "base64"
- ]
- },
- "content": {
- "type": "string",
- "title": "Attachment Text",
- "description": "The attachment data. Proactive controls such as input validation and sanitization should be employed to prevent misuse of attachment text."
- }
- }
- },
- "hash": {
- "type": "object",
- "title": "Hash Objects",
- "required": [
- "alg",
- "content"
- ],
- "additionalProperties": false,
- "properties": {
- "alg": {
- "$ref": "#/definitions/hash-alg"
- },
- "content": {
- "$ref": "#/definitions/hash-content"
- }
- }
- },
- "hash-alg": {
- "type": "string",
- "enum": [
- "MD5",
- "SHA-1",
- "SHA-256",
- "SHA-384",
- "SHA-512",
- "SHA3-256",
- "SHA3-384",
- "SHA3-512",
- "BLAKE2b-256",
- "BLAKE2b-384",
- "BLAKE2b-512",
- "BLAKE3"
- ],
- "title": "Hash Algorithm"
- },
- "hash-content": {
- "type": "string",
- "title": "Hash Content (value)",
- "examples": ["3942447fac867ae5cdb3229b658f4d48"],
- "pattern": "^([a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128})$"
- },
- "license": {
- "type": "object",
- "title": "License Object",
- "oneOf": [
- {
- "required": ["id"]
- },
- {
- "required": ["name"]
- }
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "$ref": "spdx.schema.json",
- "title": "License ID (SPDX)",
- "description": "A valid SPDX license ID",
- "examples": ["Apache-2.0"]
- },
- "name": {
- "type": "string",
- "title": "License Name",
- "description": "If SPDX does not define the license used, this field may be used to provide the license name",
- "examples": ["Acme Software License"]
- },
- "text": {
- "title": "License text",
- "description": "An optional way to include the textual content of a license.",
- "$ref": "#/definitions/attachment"
- },
- "url": {
- "type": "string",
- "title": "License URL",
- "description": "The URL to the license file. If specified, a 'license' externalReference should also be specified for completeness",
- "examples": ["https://www.apache.org/licenses/LICENSE-2.0.txt"],
- "format": "iri-reference"
- }
- }
- },
- "licenseChoice": {
- "type": "object",
- "title": "License(s)",
- "additionalProperties": false,
- "properties": {
- "license": {
- "$ref": "#/definitions/license"
- },
- "expression": {
- "type": "string",
- "title": "SPDX License Expression",
- "examples": [
- "Apache-2.0 AND (MIT OR GPL-2.0-only)",
- "GPL-3.0-only WITH Classpath-exception-2.0"
- ]
- }
- },
- "oneOf":[
- {
- "required": ["license"]
- },
- {
- "required": ["expression"]
- }
- ]
- },
- "commit": {
- "type": "object",
- "title": "Commit",
- "description": "Specifies an individual commit",
- "additionalProperties": false,
- "properties": {
- "uid": {
- "type": "string",
- "title": "UID",
- "description": "A unique identifier of the commit. This may be version control specific. For example, Subversion uses revision numbers whereas git uses commit hashes."
- },
- "url": {
- "type": "string",
- "title": "URL",
- "description": "The URL to the commit. This URL will typically point to a commit in a version control system.",
- "format": "iri-reference"
- },
- "author": {
- "title": "Author",
- "description": "The author who created the changes in the commit",
- "$ref": "#/definitions/identifiableAction"
- },
- "committer": {
- "title": "Committer",
- "description": "The person who committed or pushed the commit",
- "$ref": "#/definitions/identifiableAction"
- },
- "message": {
- "type": "string",
- "title": "Message",
- "description": "The text description of the contents of the commit"
- }
- }
- },
- "patch": {
- "type": "object",
- "title": "Patch",
- "description": "Specifies an individual patch",
- "required": [
- "type"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "type": "string",
- "enum": [
- "unofficial",
- "monkey",
- "backport",
- "cherry-pick"
- ],
- "title": "Type",
- "description": "Specifies the purpose for the patch including the resolution of defects, security issues, or new behavior or functionality.\n\n* __unofficial__ = A patch which is not developed by the creators or maintainers of the software being patched. Refer to [https://en.wikipedia.org/wiki/Unofficial_patch](https://en.wikipedia.org/wiki/Unofficial_patch)\n* __monkey__ = A patch which dynamically modifies runtime behavior. Refer to [https://en.wikipedia.org/wiki/Monkey_patch](https://en.wikipedia.org/wiki/Monkey_patch)\n* __backport__ = A patch which takes code from a newer version of software and applies it to older versions of the same software. Refer to [https://en.wikipedia.org/wiki/Backporting](https://en.wikipedia.org/wiki/Backporting)\n* __cherry-pick__ = A patch created by selectively applying commits from other versions or branches of the same software."
- },
- "diff": {
- "title": "Diff",
- "description": "The patch file (or diff) that show changes. Refer to [https://en.wikipedia.org/wiki/Diff](https://en.wikipedia.org/wiki/Diff)",
- "$ref": "#/definitions/diff"
- },
- "resolves": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/issue"},
- "title": "Resolves",
- "description": "A collection of issues the patch resolves"
- }
- }
- },
- "diff": {
- "type": "object",
- "title": "Diff",
- "description": "The patch file (or diff) that show changes. Refer to https://en.wikipedia.org/wiki/Diff",
- "additionalProperties": false,
- "properties": {
- "text": {
- "title": "Diff text",
- "description": "Specifies the optional text of the diff",
- "$ref": "#/definitions/attachment"
- },
- "url": {
- "type": "string",
- "title": "URL",
- "description": "Specifies the URL to the diff",
- "format": "iri-reference"
- }
- }
- },
- "issue": {
- "type": "object",
- "title": "Diff",
- "description": "An individual issue that has been resolved.",
- "required": [
- "type"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "type": "string",
- "enum": [
- "defect",
- "enhancement",
- "security"
- ],
- "title": "Type",
- "description": "Specifies the type of issue"
- },
- "id": {
- "type": "string",
- "title": "ID",
- "description": "The identifier of the issue assigned by the source of the issue"
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "The name of the issue"
- },
- "description": {
- "type": "string",
- "title": "Description",
- "description": "A description of the issue"
- },
- "source": {
- "type": "object",
- "title": "Source",
- "description": "The source of the issue where it is documented",
- "additionalProperties": false,
- "properties": {
- "name": {
- "type": "string",
- "title": "Name",
- "description": "The name of the source. For example 'National Vulnerability Database', 'NVD', and 'Apache'"
- },
- "url": {
- "type": "string",
- "title": "URL",
- "description": "The url of the issue documentation as provided by the source",
- "format": "iri-reference"
- }
- }
- },
- "references": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "iri-reference"
- },
- "title": "References",
- "description": "A collection of URL's for reference. Multiple URLs are allowed.",
- "examples": ["https://example.com"]
- }
- }
- },
- "identifiableAction": {
- "type": "object",
- "title": "Identifiable Action",
- "description": "Specifies an individual commit",
- "additionalProperties": false,
- "properties": {
- "timestamp": {
- "type": "string",
- "format": "date-time",
- "title": "Timestamp",
- "description": "The timestamp in which the action occurred"
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "The name of the individual who performed the action"
- },
- "email": {
- "type": "string",
- "format": "idn-email",
- "title": "E-mail",
- "description": "The email address of the individual who performed the action"
- }
- }
- },
- "externalReference": {
- "type": "object",
- "title": "External Reference",
- "description": "Specifies an individual external reference",
- "required": [
- "url",
- "type"
- ],
- "additionalProperties": false,
- "properties": {
- "url": {
- "type": "string",
- "title": "URL",
- "description": "The URL to the external reference",
- "format": "iri-reference"
- },
- "comment": {
- "type": "string",
- "title": "Comment",
- "description": "An optional comment describing the external reference"
- },
- "type": {
- "type": "string",
- "title": "Type",
- "description": "Specifies the type of external reference. There are built-in types to describe common references. If a type does not exist for the reference being referred to, use the \"other\" type.",
- "enum": [
- "vcs",
- "issue-tracker",
- "website",
- "advisories",
- "bom",
- "mailing-list",
- "social",
- "chat",
- "documentation",
- "support",
- "distribution",
- "license",
- "build-meta",
- "build-system",
- "release-notes",
- "other"
- ]
- },
- "hashes": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/hash"},
- "title": "Hashes",
- "description": "The hashes of the external reference (if applicable)."
- }
- }
- },
- "dependency": {
- "type": "object",
- "title": "Dependency",
- "description": "Defines the direct dependencies of a component. Components that do not have their own dependencies MUST be declared as empty elements within the graph. Components that are not represented in the dependency graph MAY have unknown dependencies. It is RECOMMENDED that implementations assume this to be opaque and not an indicator of a component being dependency-free.",
- "required": [
- "ref"
- ],
- "additionalProperties": false,
- "properties": {
- "ref": {
- "$ref": "#/definitions/refType",
- "title": "Reference",
- "description": "References a component by the components bom-ref attribute"
- },
- "dependsOn": {
- "type": "array",
- "uniqueItems": true,
- "additionalItems": false,
- "items": {
- "$ref": "#/definitions/refType"
- },
- "title": "Depends On",
- "description": "The bom-ref identifiers of the components that are dependencies of this dependency object."
- }
- }
- },
- "service": {
- "type": "object",
- "title": "Service Object",
- "required": [
- "name"
- ],
- "additionalProperties": false,
- "properties": {
- "bom-ref": {
- "$ref": "#/definitions/refType",
- "title": "BOM Reference",
- "description": "An optional identifier which can be used to reference the service elsewhere in the BOM. Every bom-ref MUST be unique within the BOM."
- },
- "provider": {
- "title": "Provider",
- "description": "The organization that provides the service.",
- "$ref": "#/definitions/organizationalEntity"
- },
- "group": {
- "type": "string",
- "title": "Service Group",
- "description": "The grouping name, namespace, or identifier. This will often be a shortened, single name of the company or project that produced the service or domain name. Whitespace and special characters should be avoided.",
- "examples": ["com.acme"]
- },
- "name": {
- "type": "string",
- "title": "Service Name",
- "description": "The name of the service. This will often be a shortened, single name of the service.",
- "examples": ["ticker-service"]
- },
- "version": {
- "type": "string",
- "title": "Service Version",
- "description": "The service version.",
- "examples": ["1.0.0"]
- },
- "description": {
- "type": "string",
- "title": "Service Description",
- "description": "Specifies a description for the service"
- },
- "endpoints": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "iri-reference"
- },
- "title": "Endpoints",
- "description": "The endpoint URIs of the service. Multiple endpoints are allowed.",
- "examples": ["https://example.com/api/v1/ticker"]
- },
- "authenticated": {
- "type": "boolean",
- "title": "Authentication Required",
- "description": "A boolean value indicating if the service requires authentication. A value of true indicates the service requires authentication prior to use. A value of false indicates the service does not require authentication."
- },
- "x-trust-boundary": {
- "type": "boolean",
- "title": "Crosses Trust Boundary",
- "description": "A boolean value indicating if use of the service crosses a trust zone or boundary. A value of true indicates that by using the service, a trust boundary is crossed. A value of false indicates that by using the service, a trust boundary is not crossed."
- },
- "data": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/dataClassification"},
- "title": "Data Classification",
- "description": "Specifies the data classification."
- },
- "licenses": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/licenseChoice"},
- "title": "Component License(s)"
- },
- "externalReferences": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/externalReference"},
- "title": "External References",
- "description": "External references provide a way to document systems, sites, and information that may be relevant but which are not included with the BOM."
- },
- "services": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/service"},
- "uniqueItems": true,
- "title": "Services",
- "description": "A list of services included or deployed behind the parent service. This is not a dependency tree. It provides a way to specify a hierarchical representation of service assemblies."
- },
- "releaseNotes": {
- "$ref": "#/definitions/releaseNotes",
- "title": "Release notes",
- "description": "Specifies optional release notes."
- },
- "properties": {
- "type": "array",
- "title": "Properties",
- "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/property"}
- },
- "signature": {
- "$ref": "#/definitions/signature",
- "title": "Signature",
- "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)."
- }
- }
- },
- "dataClassification": {
- "type": "object",
- "title": "Hash Objects",
- "required": [
- "flow",
- "classification"
- ],
- "additionalProperties": false,
- "properties": {
- "flow": {
- "$ref": "#/definitions/dataFlow",
- "title": "Directional Flow",
- "description": "Specifies the flow direction of the data. Direction is relative to the service. Inbound flow states that data enters the service. Outbound flow states that data leaves the service. Bi-directional states that data flows both ways, and unknown states that the direction is not known."
- },
- "classification": {
- "type": "string",
- "title": "Classification",
- "description": "Data classification tags data according to its type, sensitivity, and value if altered, stolen, or destroyed."
- }
- }
- },
- "dataFlow": {
- "type": "string",
- "enum": [
- "inbound",
- "outbound",
- "bi-directional",
- "unknown"
- ],
- "title": "Data flow direction",
- "description": "Specifies the flow direction of the data. Direction is relative to the service. Inbound flow states that data enters the service. Outbound flow states that data leaves the service. Bi-directional states that data flows both ways, and unknown states that the direction is not known."
- },
-
- "copyright": {
- "type": "object",
- "title": "Copyright",
- "required": [
- "text"
- ],
- "additionalProperties": false,
- "properties": {
- "text": {
- "type": "string",
- "title": "Copyright Text"
- }
- }
- },
-
- "componentEvidence": {
- "type": "object",
- "title": "Evidence",
- "description": "Provides the ability to document evidence collected through various forms of extraction or analysis.",
- "additionalProperties": false,
- "properties": {
- "licenses": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/licenseChoice"},
- "title": "Component License(s)"
- },
- "copyright": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/copyright"},
- "title": "Copyright"
- }
- }
- },
- "compositions": {
- "type": "object",
- "title": "Compositions",
- "required": [
- "aggregate"
- ],
- "additionalProperties": false,
- "properties": {
- "aggregate": {
- "$ref": "#/definitions/aggregateType",
- "title": "Aggregate",
- "description": "Specifies an aggregate type that describe how complete a relationship is."
- },
- "assemblies": {
- "type": "array",
- "uniqueItems": true,
- "items": {
- "type": "string"
- },
- "title": "BOM references",
- "description": "The bom-ref identifiers of the components or services being described. Assemblies refer to nested relationships whereby a constituent part may include other constituent parts. References do not cascade to child parts. References are explicit for the specified constituent part only."
- },
- "dependencies": {
- "type": "array",
- "uniqueItems": true,
- "items": {
- "type": "string"
- },
- "title": "BOM references",
- "description": "The bom-ref identifiers of the components or services being described. Dependencies refer to a relationship whereby an independent constituent part requires another independent constituent part. References do not cascade to transitive dependencies. References are explicit for the specified dependency only."
- },
- "signature": {
- "$ref": "#/definitions/signature",
- "title": "Signature",
- "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)."
- }
- }
- },
- "aggregateType": {
- "type": "string",
- "default": "not_specified",
- "enum": [
- "complete",
- "incomplete",
- "incomplete_first_party_only",
- "incomplete_third_party_only",
- "unknown",
- "not_specified"
- ]
- },
- "property": {
- "type": "object",
- "title": "Lightweight name-value pair",
- "properties": {
- "name": {
- "type": "string",
- "title": "Name",
- "description": "The name of the property. Duplicate names are allowed, each potentially having a different value."
- },
- "value": {
- "type": "string",
- "title": "Value",
- "description": "The value of the property."
- }
- }
- },
- "localeType": {
- "type": "string",
- "pattern": "^([a-z]{2})(-[A-Z]{2})?$",
- "title": "Locale",
- "description": "Defines a syntax for representing two character language code (ISO-639) followed by an optional two character country code. The language code MUST be lower case. If the country code is specified, the country code MUST be upper case. The language code and country code MUST be separated by a minus sign. Examples: en, en-US, fr, fr-CA"
- },
- "releaseType": {
- "type": "string",
- "examples": [
- "major",
- "minor",
- "patch",
- "pre-release",
- "internal"
- ],
- "description": "The software versioning type. It is RECOMMENDED that the release type use one of 'major', 'minor', 'patch', 'pre-release', or 'internal'. Representing all possible software release types is not practical, so standardizing on the recommended values, whenever possible, is strongly encouraged.\n\n* __major__ = A major release may contain significant changes or may introduce breaking changes.\n* __minor__ = A minor release, also known as an update, may contain a smaller number of changes than major releases.\n* __patch__ = Patch releases are typically unplanned and may resolve defects or important security issues.\n* __pre-release__ = A pre-release may include alpha, beta, or release candidates and typically have limited support. They provide the ability to preview a release prior to its general availability.\n* __internal__ = Internal releases are not for public consumption and are intended to be used exclusively by the project or manufacturer that produced it."
- },
- "note": {
- "type": "object",
- "title": "Note",
- "description": "A note containing the locale and content.",
- "required": [
- "text"
- ],
- "additionalProperties": false,
- "properties": {
- "locale": {
- "$ref": "#/definitions/localeType",
- "title": "Locale",
- "description": "The ISO-639 (or higher) language code and optional ISO-3166 (or higher) country code. Examples include: \"en\", \"en-US\", \"fr\" and \"fr-CA\""
- },
- "text": {
- "title": "Release note content",
- "description": "Specifies the full content of the release note.",
- "$ref": "#/definitions/attachment"
- }
- }
- },
- "releaseNotes": {
- "type": "object",
- "title": "Release notes",
- "required": [
- "type"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/releaseType",
- "title": "Type",
- "description": "The software versioning type the release note describes."
- },
- "title": {
- "type": "string",
- "title": "Title",
- "description": "The title of the release."
- },
- "featuredImage": {
- "type": "string",
- "format": "iri-reference",
- "title": "Featured image",
- "description": "The URL to an image that may be prominently displayed with the release note."
- },
- "socialImage": {
- "type": "string",
- "format": "iri-reference",
- "title": "Social image",
- "description": "The URL to an image that may be used in messaging on social media platforms."
- },
- "description": {
- "type": "string",
- "title": "Description",
- "description": "A short description of the release."
- },
- "timestamp": {
- "type": "string",
- "format": "date-time",
- "title": "Timestamp",
- "description": "The date and time (timestamp) when the release note was created."
- },
- "aliases": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "title": "Aliases",
- "description": "One or more alternate names the release may be referred to. This may include unofficial terms used by development and marketing teams (e.g. code names)."
- },
- "tags": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "title": "Tags",
- "description": "One or more tags that may aid in search or retrieval of the release note."
- },
- "resolves": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/issue"},
- "title": "Resolves",
- "description": "A collection of issues that have been resolved."
- },
- "notes": {
- "type": "array",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/note"},
- "title": "Notes",
- "description": "Zero or more release notes containing the locale and content. Multiple note objects may be specified to support release notes in a wide variety of languages."
- },
- "properties": {
- "type": "array",
- "title": "Properties",
- "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/property"}
- }
- }
- },
- "advisory": {
- "type": "object",
- "title": "Advisory",
- "description": "Title and location where advisory information can be obtained. An advisory is a notification of a threat to a component, service, or system.",
- "required": ["url"],
- "additionalProperties": false,
- "properties": {
- "title": {
- "type": "string",
- "title": "Title",
- "description": "An optional name of the advisory."
- },
- "url": {
- "type": "string",
- "title": "URL",
- "format": "iri-reference",
- "description": "Location where the advisory can be obtained."
- }
- }
- },
- "cwe": {
- "type": "integer",
- "minimum": 1,
- "title": "CWE",
- "description": "Integer representation of a Common Weaknesses Enumerations (CWE). For example 399 (of https://cwe.mitre.org/data/definitions/399.html)"
- },
- "severity": {
- "type": "string",
- "title": "Severity",
- "description": "Textual representation of the severity of the vulnerability adopted by the analysis method. If the analysis method uses values other than what is provided, the user is expected to translate appropriately.",
- "enum": [
- "critical",
- "high",
- "medium",
- "low",
- "info",
- "none",
- "unknown"
- ]
- },
- "scoreMethod": {
- "type": "string",
- "title": "Method",
- "description": "Specifies the severity or risk scoring methodology or standard used.\n\n* CVSSv2 - [Common Vulnerability Scoring System v2](https://www.first.org/cvss/v2/)\n* CVSSv3 - [Common Vulnerability Scoring System v3](https://www.first.org/cvss/v3-0/)\n* CVSSv31 - [Common Vulnerability Scoring System v3.1](https://www.first.org/cvss/v3-1/)\n* OWASP - [OWASP Risk Rating Methodology](https://owasp.org/www-community/OWASP_Risk_Rating_Methodology)",
- "enum": [
- "CVSSv2",
- "CVSSv3",
- "CVSSv31",
- "OWASP",
- "other"
- ]
- },
- "impactAnalysisState": {
- "type": "string",
- "title": "Impact Analysis State",
- "description": "Declares the current state of an occurrence of a vulnerability, after automated or manual analysis. \n\n* __resolved__ = the vulnerability has been remediated. \n* __resolved\\_with\\_pedigree__ = the vulnerability has been remediated and evidence of the changes are provided in the affected components pedigree containing verifiable commit history and/or diff(s). \n* __exploitable__ = the vulnerability may be directly or indirectly exploitable. \n* __in\\_triage__ = the vulnerability is being investigated. \n* __false\\_positive__ = the vulnerability is not specific to the component or service and was falsely identified or associated. \n* __not\\_affected__ = the component or service is not affected by the vulnerability. Justification should be specified for all not_affected cases.",
- "enum": [
- "resolved",
- "resolved_with_pedigree",
- "exploitable",
- "in_triage",
- "false_positive",
- "not_affected"
- ]
- },
- "impactAnalysisJustification": {
- "type": "string",
- "title": "Impact Analysis Justification",
- "description": "The rationale of why the impact analysis state was asserted. \n\n* __code\\_not\\_present__ = the code has been removed or tree-shaked. \n* __code\\_not\\_reachable__ = the vulnerable code is not invoked at runtime. \n* __requires\\_configuration__ = exploitability requires a configurable option to be set/unset. \n* __requires\\_dependency__ = exploitability requires a dependency that is not present. \n* __requires\\_environment__ = exploitability requires a certain environment which is not present. \n* __protected\\_by\\_compiler__ = exploitability requires a compiler flag to be set/unset. \n* __protected\\_at\\_runtime__ = exploits are prevented at runtime. \n* __protected\\_at\\_perimeter__ = attacks are blocked at physical, logical, or network perimeter. \n* __protected\\_by\\_mitigating\\_control__ = preventative measures have been implemented that reduce the likelihood and/or impact of the vulnerability.",
- "enum": [
- "code_not_present",
- "code_not_reachable",
- "requires_configuration",
- "requires_dependency",
- "requires_environment",
- "protected_by_compiler",
- "protected_at_runtime",
- "protected_at_perimeter",
- "protected_by_mitigating_control"
- ]
- },
- "rating": {
- "type": "object",
- "title": "Rating",
- "description": "Defines the severity or risk ratings of a vulnerability.",
- "additionalProperties": false,
- "properties": {
- "source": {
- "$ref": "#/definitions/vulnerabilitySource",
- "description": "The source that calculated the severity or risk rating of the vulnerability."
- },
- "score": {
- "type": "number",
- "title": "Score",
- "description": "The numerical score of the rating."
- },
- "severity": {
- "$ref": "#/definitions/severity",
- "description": "Textual representation of the severity that corresponds to the numerical score of the rating."
- },
- "method": {
- "$ref": "#/definitions/scoreMethod"
- },
- "vector": {
- "type": "string",
- "title": "Vector",
- "description": "Textual representation of the metric values used to score the vulnerability"
- },
- "justification": {
- "type": "string",
- "title": "Justification",
- "description": "An optional reason for rating the vulnerability as it was"
- }
- }
- },
- "vulnerabilitySource": {
- "type": "object",
- "title": "Source",
- "description": "The source of vulnerability information. This is often the organization that published the vulnerability.",
- "additionalProperties": false,
- "properties": {
- "url": {
- "type": "string",
- "title": "URL",
- "description": "The url of the vulnerability documentation as provided by the source.",
- "examples": [
- "https://nvd.nist.gov/vuln/detail/CVE-2021-39182"
- ]
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "The name of the source.",
- "examples": [
- "NVD",
- "National Vulnerability Database",
- "OSS Index",
- "VulnDB",
- "GitHub Advisories"
- ]
- }
- }
- },
- "vulnerability": {
- "type": "object",
- "title": "Vulnerability",
- "description": "Defines a weakness in an component or service that could be exploited or triggered by a threat source.",
- "additionalProperties": false,
- "properties": {
- "bom-ref": {
- "$ref": "#/definitions/refType",
- "title": "BOM Reference",
- "description": "An optional identifier which can be used to reference the vulnerability elsewhere in the BOM. Every bom-ref MUST be unique within the BOM."
- },
- "id": {
- "type": "string",
- "title": "ID",
- "description": "The identifier that uniquely identifies the vulnerability.",
- "examples": [
- "CVE-2021-39182",
- "GHSA-35m5-8cvj-8783",
- "SNYK-PYTHON-ENROCRYPT-1912876"
- ]
- },
- "source": {
- "$ref": "#/definitions/vulnerabilitySource",
- "description": "The source that published the vulnerability."
- },
- "references": {
- "type": "array",
- "title": "References",
- "description": "Zero or more pointers to vulnerabilities that are the equivalent of the vulnerability specified. Often times, the same vulnerability may exist in multiple sources of vulnerability intelligence, but have different identifiers. References provide a way to correlate vulnerabilities across multiple sources of vulnerability intelligence.",
- "additionalItems": false,
- "items": {
- "required": [
- "id",
- "source"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "title": "ID",
- "description": "An identifier that uniquely identifies the vulnerability.",
- "examples": [
- "CVE-2021-39182",
- "GHSA-35m5-8cvj-8783",
- "SNYK-PYTHON-ENROCRYPT-1912876"
- ]
- },
- "source": {
- "$ref": "#/definitions/vulnerabilitySource",
- "description": "The source that published the vulnerability."
- }
- }
- }
- },
- "ratings": {
- "type": "array",
- "title": "Ratings",
- "description": "List of vulnerability ratings",
- "additionalItems": false,
- "items": {
- "$ref": "#/definitions/rating"
- }
- },
- "cwes": {
- "type": "array",
- "title": "CWEs",
- "description": "List of Common Weaknesses Enumerations (CWEs) codes that describes this vulnerability. For example 399 (of https://cwe.mitre.org/data/definitions/399.html)",
- "examples": ["399"],
- "additionalItems": false,
- "items": {
- "$ref": "#/definitions/cwe"
- }
- },
- "description": {
- "type": "string",
- "title": "Description",
- "description": "A description of the vulnerability as provided by the source."
- },
- "detail": {
- "type": "string",
- "title": "Details",
- "description": "If available, an in-depth description of the vulnerability as provided by the source organization. Details often include examples, proof-of-concepts, and other information useful in understanding root cause."
- },
- "recommendation": {
- "type": "string",
- "title": "Details",
- "description": "Recommendations of how the vulnerability can be remediated or mitigated."
- },
- "advisories": {
- "type": "array",
- "title": "Advisories",
- "description": "Published advisories of the vulnerability if provided.",
- "additionalItems": false,
- "items": {
- "$ref": "#/definitions/advisory"
- }
- },
- "created": {
- "type": "string",
- "format": "date-time",
- "title": "Created",
- "description": "The date and time (timestamp) when the vulnerability record was created in the vulnerability database."
- },
- "published": {
- "type": "string",
- "format": "date-time",
- "title": "Published",
- "description": "The date and time (timestamp) when the vulnerability record was first published."
- },
- "updated": {
- "type": "string",
- "format": "date-time",
- "title": "Updated",
- "description": "The date and time (timestamp) when the vulnerability record was last updated."
- },
- "credits": {
- "type": "object",
- "title": "Credits",
- "description": "Individuals or organizations credited with the discovery of the vulnerability.",
- "additionalProperties": false,
- "properties": {
- "organizations": {
- "type": "array",
- "title": "Organizations",
- "description": "The organizations credited with vulnerability discovery.",
- "additionalItems": false,
- "items": {
- "$ref": "#/definitions/organizationalEntity"
- }
- },
- "individuals": {
- "type": "array",
- "title": "Individuals",
- "description": "The individuals, not associated with organizations, that are credited with vulnerability discovery.",
- "additionalItems": false,
- "items": {
- "$ref": "#/definitions/organizationalContact"
- }
- }
- }
- },
- "tools": {
- "type": "array",
- "title": "Creation Tools",
- "description": "The tool(s) used to identify, confirm, or score the vulnerability.",
- "additionalItems": false,
- "items": {"$ref": "#/definitions/tool"}
- },
- "analysis": {
- "type": "object",
- "title": "Impact Analysis",
- "description": "An assessment of the impact and exploitability of the vulnerability.",
- "additionalProperties": false,
- "properties": {
- "state": {
- "$ref": "#/definitions/impactAnalysisState"
- },
- "justification": {
- "$ref": "#/definitions/impactAnalysisJustification"
- },
- "response": {
- "type": "array",
- "title": "Response",
- "description": "A response to the vulnerability by the manufacturer, supplier, or project responsible for the affected component or service. More than one response is allowed. Responses are strongly encouraged for vulnerabilities where the analysis state is exploitable.",
- "additionalItems": false,
- "items": {
- "type": "string",
- "enum": [
- "can_not_fix",
- "will_not_fix",
- "update",
- "rollback",
- "workaround_available"
- ]
- }
- },
- "detail": {
- "type": "string",
- "title": "Detail",
- "description": "Detailed description of the impact including methods used during assessment. If a vulnerability is not exploitable, this field should include specific details on why the component or service is not impacted by this vulnerability."
- }
- }
- },
- "affects": {
- "type": "array",
- "uniqueItems": true,
- "additionalItems": false,
- "items": {
- "required": [
- "ref"
- ],
- "additionalProperties": false,
- "properties": {
- "ref": {
- "$ref": "#/definitions/refType",
- "title": "Reference",
- "description": "References a component or service by the objects bom-ref"
- },
- "versions": {
- "type": "array",
- "title": "Versions",
- "description": "Zero or more individual versions or range of versions.",
- "additionalItems": false,
- "items": {
- "oneOf": [
- {
- "required": ["version"]
- },
- {
- "required": ["range"]
- }
- ],
- "additionalProperties": false,
- "properties": {
- "version": {
- "description": "A single version of a component or service.",
- "$ref": "#/definitions/version"
- },
- "range": {
- "description": "A version range specified in Package URL Version Range syntax (vers) which is defined at https://github.com/package-url/purl-spec/VERSION-RANGE-SPEC.rst",
- "$ref": "#/definitions/version"
- },
- "status": {
- "description": "The vulnerability status for the version or range of versions.",
- "$ref": "#/definitions/affectedStatus",
- "default": "affected"
- }
- }
- }
- }
- }
- },
- "title": "Affects",
- "description": "The components or services that are affected by the vulnerability."
- },
- "properties": {
- "type": "array",
- "title": "Properties",
- "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.",
- "additionalItems": false,
- "items": {
- "$ref": "#/definitions/property"
- }
- }
- }
- },
- "affectedStatus": {
- "description": "The vulnerability status of a given version or range of versions of a product. The statuses 'affected' and 'unaffected' indicate that the version is affected or unaffected by the vulnerability. The status 'unknown' indicates that it is unknown or unspecified whether the given version is affected. There can be many reasons for an 'unknown' status, including that an investigation has not been undertaken or that a vendor has not disclosed the status.",
- "type": "string",
- "enum": [
- "affected",
- "unaffected",
- "unknown"
- ]
- },
- "version": {
- "description": "A single version of a component or service.",
- "type": "string",
- "minLength": 1,
- "maxLength": 1024
- },
- "range": {
- "description": "A version range specified in Package URL Version Range syntax (vers) which is defined at https://github.com/package-url/purl-spec/VERSION-RANGE-SPEC.rst",
- "type": "string",
- "minLength": 1,
- "maxLength": 1024
- },
- "signature": {
- "$ref": "jsf-0.82.schema.json#/definitions/signature",
- "title": "Signature",
- "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)."
- }
- }
- }
diff --git a/schema/cyclonedx/cyclonedx.xsd b/schema/cyclonedx/cyclonedx.xsd
deleted file mode 100644
index a0218439f74..00000000000
--- a/schema/cyclonedx/cyclonedx.xsd
+++ /dev/null
@@ -1,2417 +0,0 @@
-
-
-
-
-
-
-
-
- CycloneDX Software Bill of Materials Standard
- https://cyclonedx.org/
- Apache License, Version 2.0
-
-
-
-
-
- Identifier-DataType for interlinked elements.
-
-
-
-
-
-
-
-
- The date and time (timestamp) when the BOM was created.
-
-
-
-
- The tool(s) used in the creation of the BOM.
-
-
-
-
-
-
-
-
-
- The person(s) who created the BOM. Authors are common in BOMs created through
- manual processes. BOMs created through automated means may not have authors.
-
-
-
-
-
-
-
-
-
- The component that the BOM describes.
-
-
-
-
- The organization that manufactured the component that the BOM describes.
-
-
-
-
- The organization that supplied the component that the BOM describes. The
- supplier may often be the manufacturer, but may also be a distributor or repackager.
-
-
-
-
-
- Provides the ability to document properties in a key/value store.
- This provides flexibility to include data not officially supported in the standard
- without having to use additional namespaces or create extensions. Property names
- of interest to the general public are encouraged to be registered in the
- CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy.
- Formal registration is OPTIONAL.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
- The name of the organization
-
-
-
-
- The URL of the organization. Multiple URLs are allowed.
-
-
-
-
- A contact person at the organization. Multiple contacts are allowed.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
- Information about the automated or manual tool used
-
-
-
-
- The name of the vendor who created the tool
-
-
-
-
- The name of the tool
-
-
-
-
- The version of the tool
-
-
-
-
-
-
-
-
-
-
-
- Provides the ability to document external references related to the tool.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
- The name of the contact
-
-
-
-
- The email address of the contact.
-
-
-
-
- The phone number of the contact.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
- The organization that supplied the component. The supplier may often
- be the manufacturer, but may also be a distributor or repackager.
-
-
-
-
- The person(s) or organization(s) that authored the component
-
-
-
-
- The person(s) or organization(s) that published the component
-
-
-
-
- The grouping name or identifier. This will often be a shortened, single
- name of the company or project that produced the component, or the source package or
- domain name. Whitespace and special characters should be avoided. Examples include:
- apache, org.apache.commons, and apache.org.
-
-
-
-
- The name of the component. This will often be a shortened, single name
- of the component. Examples: commons-lang3 and jquery
-
-
-
-
- The component version. The version should ideally comply with semantic versioning
- but is not enforced.
-
-
-
-
- Specifies a description for the component
-
-
-
-
- Specifies the scope of the component. If scope is not specified, 'required'
- scope SHOULD be assumed by the consumer of the BOM.
-
-
-
-
-
-
-
-
-
-
-
-
- A copyright notice informing users of the underlying claims to
- copyright ownership in a published work.
-
-
-
-
-
- Specifies a well-formed CPE name that conforms to the CPE 2.2 or 2.3 specification. See https://nvd.nist.gov/products/cpe
-
-
-
-
-
-
- Specifies the package-url (purl). The purl, if specified, MUST be valid and conform
- to the specification defined at: https://github.com/package-url/purl-spec
-
-
-
-
-
-
- Specifies metadata and content for ISO-IEC 19770-2 Software Identification (SWID) Tags.
-
-
-
-
-
-
- DEPRECATED - DO NOT USE. This will be removed in a future version. Use the pedigree
- element instead to supply information on exactly how the component was modified.
- A boolean value indicating if the component has been modified from the original.
- A value of true indicates the component is a derivative of the original.
- A value of false indicates the component has not been modified from the original.
-
-
-
-
-
-
- Component pedigree is a way to document complex supply chain scenarios where components are
- created, distributed, modified, redistributed, combined with other components, etc.
-
-
-
-
-
- Provides the ability to document external references related to the
- component or to the project the component describes.
-
-
-
-
- Provides the ability to document properties in a key/value store.
- This provides flexibility to include data not officially supported in the standard
- without having to use additional namespaces or create extensions. Property names
- of interest to the general public are encouraged to be registered in the
- CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy.
- Formal registration is OPTIONAL.
-
-
-
-
-
- A list of software and hardware components included in the parent component. This is not a
- dependency tree. It provides a way to specify a hierarchical representation of component
- assemblies, similar to system -> subsystem -> parts assembly in physical supply chains.
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
- Provides the ability to document evidence collected through various forms of extraction or analysis.
-
-
-
-
- Specifies optional release notes.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
- Specifies the type of component. For software components, classify as application if no more
- specific appropriate classification is available or cannot be determined for the component.
-
-
-
-
-
-
- The OPTIONAL mime-type of the component. When used on file components, the mime-type
- can provide additional context about the kind of file being represented such as an image,
- font, or executable. Some library or framework components may also have an associated mime-type.
-
-
-
-
-
-
- An optional identifier which can be used to reference the component elsewhere in the BOM.
- Uniqueness is enforced within all elements and children of the root-level bom element.
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
-
- A valid SPDX license ID
-
-
-
-
- If SPDX does not define the license used, this field may be used to provide the license name
-
-
-
-
-
- Specifies the optional full text of the attachment
-
-
-
-
- The URL to the attachment file. If the attachment is a license or BOM,
- an externalReference should also be specified for completeness.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
-
-
- The attachment data. Proactive controls such as input validation and sanitization should be employed to prevent misuse of attachment text.
-
-
-
- Specifies the content type of the text. Defaults to text/plain
- if not specified.
-
-
-
-
-
- Specifies the optional encoding the text is represented in
-
-
-
-
-
-
-
-
-
- Specifies the file hash of the component
-
-
-
-
-
- Specifies the algorithm used to create the hash
-
-
-
-
-
-
-
-
-
-
- The component is required for runtime
-
-
-
-
- The component is optional at runtime. Optional components are components that
- are not capable of being called due to them not be installed or otherwise accessible by any means.
- Components that are installed but due to configuration or other restrictions are prohibited from
- being called must be scoped as 'required'.
-
-
-
-
- Components that are excluded provide the ability to document component usage
- for test and other non-runtime purposes. Excluded components are not reachable within a call
- graph at runtime.
-
-
-
-
-
-
-
-
-
- A software application. Refer to https://en.wikipedia.org/wiki/Application_software
- for information about applications.
-
-
-
-
- A software framework. Refer to https://en.wikipedia.org/wiki/Software_framework
- for information on how frameworks vary slightly from libraries.
-
-
-
-
- A software library. Refer to https://en.wikipedia.org/wiki/Library_(computing)
- for information about libraries. All third-party and open source reusable components will likely
- be a library. If the library also has key features of a framework, then it should be classified
- as a framework. If not, or is unknown, then specifying library is recommended.
-
-
-
-
- A packaging and/or runtime format, not specific to any particular technology,
- which isolates software inside the container from software outside of a container through
- virtualization technology. Refer to https://en.wikipedia.org/wiki/OS-level_virtualization
-
-
-
-
- A software operating system without regard to deployment model
- (i.e. installed on physical hardware, virtual machine, image, etc) Refer to
- https://en.wikipedia.org/wiki/Operating_system
-
-
-
-
- A hardware device such as a processor, or chip-set. A hardware device
- containing firmware SHOULD include a component for the physical hardware itself, and another
- component of type 'firmware' or 'operating-system' (whichever is relevant), describing
- information about the software running on the device.
-
-
-
-
- A special type of software that provides low-level control over a devices
- hardware. Refer to https://en.wikipedia.org/wiki/Firmware
-
-
-
-
- A computer file. Refer to https://en.wikipedia.org/wiki/Computer_file
- for information about files.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Define the format for acceptable CPE URIs. Supports CPE 2.2 and CPE 2.3 formats.
- Refer to https://nvd.nist.gov/products/cpe for official specification.
-
-
-
-
-
-
-
-
-
-
-
- Specifies the full content of the SWID tag.
-
-
-
-
- The URL to the SWID file.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- Maps to the tagId of a SoftwareIdentity.
-
-
-
-
- Maps to the name of a SoftwareIdentity.
-
-
-
-
- Maps to the version of a SoftwareIdentity.
-
-
-
-
- Maps to the tagVersion of a SoftwareIdentity.
-
-
-
-
- Maps to the patch of a SoftwareIdentity.
-
-
-
-
-
-
-
- Defines a string representation of a UUID conforming to RFC 4122.
-
-
-
-
-
-
-
-
-
-
-
- Version Control System
-
-
-
-
- Issue or defect tracking system, or an Application Lifecycle Management (ALM) system
-
-
-
-
- Website
-
-
-
-
- Security advisories
-
-
-
-
- Bill-of-material document (CycloneDX, SPDX, SWID, etc)
-
-
-
-
- Mailing list or discussion group
-
-
-
-
- Social media account
-
-
-
-
- Real-time chat platform
-
-
-
-
- Documentation, guides, or how-to instructions
-
-
-
-
- Community or commercial support
-
-
-
-
- Direct or repository download location
-
-
-
-
- The URL to the license file. If a license URL has been defined in the license
- node, it should also be defined as an external reference for completeness
-
-
-
-
- Build-system specific meta file (i.e. pom.xml, package.json, .nuspec, etc)
-
-
-
-
- URL to an automated build system
-
-
-
-
- URL to release notes
-
-
-
-
- Use this if no other types accurately describe the purpose of the external reference
-
-
-
-
-
-
-
-
- External references provide a way to document systems, sites, and information that may be relevant
- but which are not included with the BOM.
-
-
-
-
-
- Zero or more external references can be defined
-
-
-
-
-
-
-
-
-
- The URL to the external reference
-
-
-
-
- An optional comment describing the external reference
-
-
-
-
-
-
-
-
-
-
-
-
- Specifies the type of external reference. There are built-in types to describe common
- references. If a type does not exist for the reference being referred to, use the "other" type.
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
- Zero or more commits can be specified.
-
-
-
-
- Specifies an individual commit.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
-
-
- A unique identifier of the commit. This may be version control
- specific. For example, Subversion uses revision numbers whereas git uses commit hashes.
-
-
-
-
-
- The URL to the commit. This URL will typically point to a commit
- in a version control system.
-
-
-
-
-
- The author who created the changes in the commit
-
-
-
-
- The person who committed or pushed the commit
-
-
-
-
- The text description of the contents of the commit
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
- Zero or more patches can be specified.
-
-
-
-
- Specifies an individual patch.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
-
-
- The patch file (or diff) that show changes.
- Refer to https://en.wikipedia.org/wiki/Diff
-
-
-
-
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- Specifies the purpose for the patch including the resolution of defects,
- security issues, or new behavior or functionality
-
-
-
-
-
-
-
-
- A patch which is not developed by the creators or maintainers of the software
- being patched. Refer to https://en.wikipedia.org/wiki/Unofficial_patch
-
-
-
-
- A patch which dynamically modifies runtime behavior.
- Refer to https://en.wikipedia.org/wiki/Monkey_patch
-
-
-
-
- A patch which takes code from a newer version of software and applies
- it to older versions of the same software. Refer to https://en.wikipedia.org/wiki/Backporting
-
-
-
-
- A patch created by selectively applying commits from other versions or
- branches of the same software.
-
-
-
-
-
-
-
-
-
- A fault, flaw, or bug in software
-
-
-
-
- A new feature or behavior in software
-
-
-
-
- A special type of defect which impacts security
-
-
-
-
-
-
-
-
-
- Specifies the optional text of the diff
-
-
-
-
- Specifies the URL to the diff
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
-
- An individual issue that has been resolved.
-
-
-
-
-
- The identifier of the issue assigned by the source of the issue
-
-
-
-
- The name of the issue
-
-
-
-
- A description of the issue
-
-
-
-
-
-
- The source of the issue where it is documented.
-
-
-
-
-
-
- The name of the source. For example "National Vulnerability Database",
- "NVD", and "Apache"
-
-
-
-
-
-
- The url of the issue documentation as provided by the source
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- Specifies the type of issue
-
-
-
-
-
-
-
-
- The timestamp in which the action occurred
-
-
-
-
- The name of the individual who performed the action
-
-
-
-
- The email address of the individual who performed the action
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
-
- Component pedigree is a way to document complex supply chain scenarios where components are created,
- distributed, modified, redistributed, combined with other components, etc. Pedigree supports viewing
- this complex chain from the beginning, the end, or anywhere in the middle. It also provides a way to
- document variants where the exact relation may not be known.
-
-
-
-
-
- Describes zero or more components in which a component is derived
- from. This is commonly used to describe forks from existing projects where the forked version
- contains a ancestor node containing the original component it was forked from. For example,
- Component A is the original component. Component B is the component being used and documented
- in the BOM. However, Component B contains a pedigree node with a single ancestor documenting
- Component A - the original component from which Component B is derived from.
-
-
-
-
-
- Descendants are the exact opposite of ancestors. This provides a
- way to document all forks (and their forks) of an original or root component.
-
-
-
-
-
- Variants describe relations where the relationship between the
- components are not known. For example, if Component A contains nearly identical code to
- Component B. They are both related, but it is unclear if one is derived from the other,
- or if they share a common ancestor.
-
-
-
-
-
- A list of zero or more commits which provide a trail describing
- how the component deviates from an ancestor, descendant, or variant.
-
-
-
-
- A list of zero or more patches describing how the component
- deviates from an ancestor, descendant, or variant. Patches may be complimentary to commits
- or may be used in place of commits.
-
-
-
-
- Notes, observations, and other non-structured commentary
- describing the components pedigree.
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
-
-
-
-
- References a component or service by the its bom-ref attribute
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
- Components that do not have their own dependencies MUST be declared as empty
- elements within the graph. Components that are not represented in the dependency graph MAY
- have unknown dependencies. It is RECOMMENDED that implementations assume this to be opaque
- and not an indicator of a component being dependency-free.
-
-
-
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
- The organization that provides the service.
-
-
-
-
- The grouping name, namespace, or identifier. This will often be a shortened,
- single name of the company or project that produced the service or domain name.
- Whitespace and special characters should be avoided.
-
-
-
-
- The name of the service. This will often be a shortened, single name
- of the service.
-
-
-
-
- The service version.
-
-
-
-
- Specifies a description for the service.
-
-
-
-
-
-
-
- A service endpoint URI.
-
-
-
-
-
-
-
- A boolean value indicating if the service requires authentication.
- A value of true indicates the service requires authentication prior to use.
- A value of false indicates the service does not require authentication.
-
-
-
-
- A boolean value indicating if use of the service crosses a trust zone or boundary.
- A value of true indicates that by using the service, a trust boundary is crossed.
- A value of false indicates that by using the service, a trust boundary is not crossed.
-
-
-
-
-
-
-
- Specifies the data classification.
-
-
-
-
-
-
-
-
- Provides the ability to document external references related to the service.
-
-
-
-
- Provides the ability to document properties in a key/value store.
- This provides flexibility to include data not officially supported in the standard
- without having to use additional namespaces or create extensions. Property names
- of interest to the general public are encouraged to be registered in the
- CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy.
- Formal registration is OPTIONAL.
-
-
-
-
-
- A list of services included or deployed behind the parent service. This is not a dependency
- tree. It provides a way to specify a hierarchical representation of service assemblies.
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
- Specifies optional release notes.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
- An optional identifier which can be used to reference the service elsewhere in the BOM.
- Uniqueness is enforced within all elements and children of the root-level bom element.
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
- Specifies the data classification.
-
-
-
-
-
- Specifies the flow direction of the data.
-
-
-
-
-
-
-
-
- Specifies the flow direction of the data. Valid values are:
- inbound, outbound, bi-directional, and unknown. Direction is relative to the service.
- Inbound flow states that data enters the service. Outbound flow states that data
- leaves the service. Bi-directional states that data flows both ways, and unknown
- states that the direction is not known.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- A valid SPDX license expression.
- Refer to https://spdx.org/specifications for syntax requirements
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
- Specifies an aggregate type that describe how complete a relationship is.
-
-
-
-
-
- The bom-ref identifiers of the components or services being described. Assemblies refer to
- nested relationships whereby a constituent part may include other constituent parts. References
- do not cascade to child parts. References are explicit for the specified constituent part only.
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
-
- The bom-ref identifiers of the components or services being described. Dependencies refer to a
- relationship whereby an independent constituent part requires another independent constituent
- part. References do not cascade to transitive dependencies. References are explicit for the
- specified dependency only.
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
-
-
-
-
-
- The relationship is complete. No further relationships including constituent components, services, or dependencies exist.
-
-
-
-
- The relationship is incomplete. Additional relationships exist and may include constituent components, services, or dependencies.
-
-
-
-
- The relationship is incomplete. Only relationships for first-party components, services, or their dependencies are represented.
-
-
-
-
- The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented.
-
-
-
-
- The relationship may be complete or incomplete. This usually signifies a 'best-effort' to obtain constituent components, services, or dependencies but the completeness is inconclusive.
-
-
-
-
- The relationship completeness is not specified.
-
-
-
-
-
-
-
-
- Defines a syntax for representing two character language code (ISO-639) followed by an optional two
- character country code. The language code MUST be lower case. If the country code is specified, the
- country code MUST be upper case. The language code and country code MUST be separated by a minus sign.
- Examples: en, en-US, fr, fr-CA
-
-
-
-
-
-
-
-
-
-
-
- The software versioning type. It is RECOMMENDED that the release type use one
- of 'major', 'minor', 'patch', 'pre-release', or 'internal'. Representing all possible software
- release types is not practical, so standardizing on the recommended values, whenever possible,
- is strongly encouraged.
- * major = A major release may contain significant changes or may introduce breaking changes.
- * minor = A minor release, also known as an update, may contain a smaller number of changes than major releases.
- * patch = Patch releases are typically unplanned and may resolve defects or important security issues.
- * pre-release = A pre-release may include alpha, beta, or release candidates and typically have
- limited support. They provide the ability to preview a release prior to its general availability.
- * internal = Internal releases are not for public consumption and are intended to be used exclusively
- by the project or manufacturer that produced it.
-
-
-
-
-
- The title of the release.
-
-
-
-
- The URL to an image that may be prominently displayed with the release note.
-
-
-
-
- The URL to an image that may be used in messaging on social media platforms.
-
-
-
-
- A short description of the release.
-
-
-
-
- The date and time (timestamp) when the release note was created.
-
-
-
-
-
-
-
- One or more alternate names the release may be referred to. This may
- include unofficial terms used by development and marketing teams (e.g. code names).
-
-
-
-
-
-
-
-
-
-
- One or more tags that may aid in search or retrieval of the release note.
-
-
-
-
-
-
-
- A collection of issues that have been resolved.
-
-
-
-
-
-
-
-
-
-
-
-
- Zero or more release notes containing the locale and content. Multiple
- note elements may be specified to support release notes in a wide variety of languages.
-
-
-
-
-
- The ISO-639 (or higher) language code and optional ISO-3166
- (or higher) country code. Examples include: "en", "en-US", "fr" and "fr-CA".
-
-
-
-
- Specifies the full content of the release note.
-
-
-
-
-
-
-
-
-
-
- Provides the ability to document properties in a key/value store.
- This provides flexibility to include data not officially supported in the standard
- without having to use additional namespaces or create extensions. Property names
- of interest to the general public are encouraged to be registered in the
- CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy.
- Formal registration is OPTIONAL.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
- References a component or service by the its bom-ref attribute
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
- Specifies an individual property with a name and value.
-
-
-
-
-
- The name of the property. Duplicate names are allowed, each potentially having a different value.
-
-
-
-
-
-
-
-
-
-
- Defines a weakness in an component or service that could be exploited or triggered by a threat source.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
- The identifier that uniquely identifies the vulnerability. For example:
- CVE-2021-39182, GHSA-35m5-8cvj-8783, and SNYK-PYTHON-ENROCRYPT-1912876.
-
-
-
-
- The source that published the vulnerability.
-
-
-
-
- Zero or more pointers to vulnerabilities that are the equivalent of the
- vulnerability specified. Often times, the same vulnerability may exist in multiple sources of
- vulnerability intelligence, but have different identifiers. References provide a way to
- correlate vulnerabilities across multiple sources of vulnerability intelligence.
-
-
-
-
-
- A pointer to a vulnerability that is the equivalent of the
- vulnerability specified.
-
-
-
-
-
- The identifier that uniquely identifies the vulnerability. For example:
- CVE-2021-39182, GHSA-35m5-8cvj-8783, and SNYK-PYTHON-ENROCRYPT-1912876.
-
-
-
-
- The source that published the vulnerability.
-
-
-
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
-
-
- List of vulnerability ratings.
-
-
-
-
-
-
-
-
-
-
-
- List of Common Weaknesses Enumerations (CWEs) codes that describes this vulnerability.
- For example 399 (of https://cwe.mitre.org/data/definitions/399.html)
-
-
-
-
-
-
-
-
-
- A description of the vulnerability as provided by the source.
-
-
-
-
- If available, an in-depth description of the vulnerability as provided by the
- source organization. Details often include examples, proof-of-concepts, and other information
- useful in understanding root cause.
-
-
-
-
- Recommendations of how the vulnerability can be remediated or mitigated.
-
-
-
-
-
-
- Published advisories of the vulnerability if provided.
-
-
-
-
-
-
-
-
-
- The date and time (timestamp) when the vulnerability record was created in the vulnerability database.
-
-
-
-
- The date and time (timestamp) when the vulnerability record was first published.
-
-
-
-
- The date and time (timestamp) when the vulnerability record was last updated.
-
-
-
-
- Individuals or organizations credited with the discovery of the vulnerability.
-
-
-
-
-
- The organizations credited with vulnerability discovery.
-
-
-
-
-
-
-
-
-
- The individuals, not associated with organizations, that are credited with vulnerability discovery.
-
-
-
-
-
-
-
-
-
-
-
-
- The tool(s) used to identify, confirm, or score the vulnerability.
-
-
-
-
-
-
-
-
-
-
-
- An assessment of the impact and exploitability of the vulnerability.
-
-
-
-
-
-
- Declares the current state of an occurrence of a vulnerability, after automated or manual analysis.
-
-
-
-
-
-
- The rationale of why the impact analysis state was asserted.
-
-
-
-
-
- A response to the vulnerability by the manufacturer, supplier, or
- project responsible for the affected component or service. More than one response
- is allowed. Responses are strongly encouraged for vulnerabilities where the analysis
- state is exploitable.
-
-
-
-
-
-
-
-
-
-
- Detailed description of the impact including methods used during assessment.
- If a vulnerability is not exploitable, this field should include specific details
- on why the component or service is not impacted by this vulnerability.
-
-
-
-
-
-
-
-
- The components or services that are affected by the vulnerability.
-
-
-
-
-
-
-
-
- References a component or service by the objects bom-ref.
-
-
-
-
- Zero or more individual versions or range of versions.
-
-
-
-
-
-
-
-
-
- A single version of a component or service.
-
-
-
-
- A version range specified in Package URL Version Range syntax (vers) which is defined at https://github.com/package-url/purl-spec/VERSION-RANGE-SPEC.rst
-
-
-
-
-
-
- The vulnerability status for the version or range of versions.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Provides the ability to document properties in a key/value store.
- This provides flexibility to include data not officially supported in the standard
- without having to use additional namespaces or create extensions. Property names
- of interest to the general public are encouraged to be registered in the
- CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy.
- Formal registration is OPTIONAL.
-
-
-
-
-
-
- An optional identifier which can be used to reference the vulnerability elsewhere in the BOM.
- Uniqueness is enforced within all elements and children of the root-level bom element.
-
-
-
-
-
-
-
-
-
- The name of the source.
- For example: NVD, National Vulnerability Database, OSS Index, VulnDB, and GitHub Advisories
-
-
-
-
-
- The url of the vulnerability documentation as provided by the source.
- For example: https://nvd.nist.gov/vuln/detail/CVE-2021-39182
-
-
-
-
-
-
-
-
-
- The source that calculated the severity or risk rating of the vulnerability.
-
-
-
-
- The numerical score of the rating.
-
-
-
-
- Textual representation of the severity that corresponds to the numerical score of the rating.
-
-
-
-
- The risk scoring methodology/standard used.
-
-
-
-
- Textual representation of the metric values used to score the vulnerability.
-
-
-
-
- An optional reason for rating the vulnerability as it was.
-
-
-
-
-
-
-
-
-
- An optional name of the advisory.
-
-
-
-
- Location where the advisory can be obtained.
-
-
-
-
-
-
-
-
- Textual representation of the severity of the vulnerability adopted by the analysis method. If the
- analysis method uses values other than what is provided, the user is expected to translate appropriately.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Declares the current state of an occurrence of a vulnerability, after automated or manual analysis.
-
-
-
-
-
-
- The vulnerability has been remediated.
-
-
-
-
-
-
- The vulnerability has been remediated and evidence of the changes are provided in the affected
- components pedigree containing verifiable commit history and/or diff(s).
-
-
-
-
-
-
- The vulnerability may be directly or indirectly exploitable.
-
-
-
-
-
-
- The vulnerability is being investigated.
-
-
-
-
-
-
- The vulnerability is not specific to the component or service and was falsely identified or associated.
-
-
-
-
-
-
- The component or service is not affected by the vulnerability. Justification should be specified
- for all not_affected cases.
-
-
-
-
-
-
-
-
-
- The rationale of why the impact analysis state was asserted.
-
-
-
-
-
-
- The code has been removed or tree-shaked.
-
-
-
-
-
-
- The vulnerable code is not invoked at runtime.
-
-
-
-
-
-
- Exploitability requires a configurable option to be set/unset.
-
-
-
-
-
-
- Exploitability requires a dependency that is not present.
-
-
-
-
-
-
- Exploitability requires a certain environment which is not present.
-
-
-
-
-
-
- Exploitability requires a compiler flag to be set/unset.
-
-
-
-
-
-
- Exploits are prevented at runtime.
-
-
-
-
-
-
- Attacks are blocked at physical, logical, or network perimeter.
-
-
-
-
-
-
- Preventative measures have been implemented that reduce the likelihood and/or impact of the vulnerability.
-
-
-
-
-
-
-
-
-
- Specifies the severity or risk scoring methodology or standard used.
-
-
-
-
-
-
- The rating is based on CVSS v2 standard
- https://www.first.org/cvss/v2/
-
-
-
-
-
-
- The rating is based on CVSS v3.0 standard
- https://www.first.org/cvss/v3-0/
-
-
-
-
-
-
- The rating is based on CVSS v3.1 standard
- https://www.first.org/cvss/v3-1/
-
-
-
-
-
-
- The rating is based on OWASP Risk Rating
- https://owasp.org/www-community/OWASP_Risk_Rating_Methodology
-
-
-
-
-
-
- Use this if the risk scoring methodology is not based on any of the options above
-
-
-
-
-
-
-
-
-
- The rationale of why the impact analysis state was asserted.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- The vulnerability status of a given version or range of versions of a product. The statuses
- 'affected' and 'unaffected' indicate that the version is affected or unaffected by the vulnerability.
- The status 'unknown' indicates that it is unknown or unspecified whether the given version is affected.
- There can be many reasons for an 'unknown' status, including that an investigation has not been
- undertaken or that a vendor has not disclosed the status.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Provides additional information about a BOM.
-
-
-
-
- A list of software and hardware components.
-
-
-
-
- A list of services. This may include microservices, function-as-a-service, and other types of network or intra-process services.
-
-
-
-
- Provides the ability to document external references related to the BOM or
- to the project the BOM describes.
-
-
-
-
- Provides the ability to document dependency relationships.
-
-
-
-
- Compositions describe constituent parts (including components, services, and dependency relationships) and their completeness.
-
-
-
-
- Provides the ability to document properties in a key/value store.
- This provides flexibility to include data not officially supported in the standard
- without having to use additional namespaces or create extensions. Property names
- of interest to the general public are encouraged to be registered in the
- CycloneDX Property Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy.
- Formal registration is OPTIONAL.
-
-
-
-
- Vulnerabilities identified in components or services.
-
-
-
-
-
- Allows any undeclared elements as long as the elements are placed in a different namespace.
-
-
-
-
-
-
- Whenever an existing BOM is modified, either manually or through automated
- processes, the version of the BOM SHOULD be incremented by 1. When a system is presented with
- multiple BOMs with identical serial numbers, the system SHOULD use the most recent version of the BOM.
- The default version is '1'.
-
-
-
-
- Every BOM generated SHOULD have a unique serial number, even if the contents of
- the BOM have not changed over time. If specified, the serial number MUST conform to RFC-4122.
- Use of serial numbers are RECOMMENDED.
-
-
-
-
- User-defined attributes may be used on this element as long as they
- do not have the same name as an existing attribute used by the schema.
-
-
-
-
-
-
-
-
-
diff --git a/schema/cyclonedx/spdx.xsd b/schema/cyclonedx/spdx.xsd
deleted file mode 100644
index 66ba6f199e7..00000000000
--- a/schema/cyclonedx/spdx.xsd
+++ /dev/null
@@ -1,2639 +0,0 @@
-
-
-
-
-
-
-
-
- Interbase Public License v1.0
-
-
-
-
- Mup License
-
-
-
-
- GNU General Public License v2.0 w/Autoconf exception
-
-
-
-
- Open LDAP Public License v2.1
-
-
-
-
- Creative Commons Attribution Non Commercial Share Alike 3.0 IGO
-
-
-
-
- GNU Library General Public License v2 or later
-
-
-
-
- XPP License
-
-
-
-
- SIL Open Font License 1.1
-
-
-
-
- CNRI Python License
-
-
-
-
- Linux man-pages Copyleft
-
-
-
-
- Open LDAP Public License v2.2
-
-
-
-
- Open Software License 1.1
-
-
-
-
- Eclipse Public License 2.0
-
-
-
-
- Academic Free License v1.1
-
-
-
-
- Affero General Public License v1.0 or later
-
-
-
-
- Good Luck With That Public License
-
-
-
-
- MIT License Modern Variant
-
-
-
-
- BSD 1-Clause License
-
-
-
-
- SGI Free Software License B v1.0
-
-
-
-
- Open Market License
-
-
-
-
- psfrag License
-
-
-
-
- Artistic License 1.0
-
-
-
-
- Creative Commons Public Domain Dedication and Certification
-
-
-
-
- eGenix.com Public License 1.1.0
-
-
-
-
- European Union Public License 1.1
-
-
-
-
- Sendmail License
-
-
-
-
- Python Software Foundation License 2.0
-
-
-
-
- Open Government Licence v1.0
-
-
-
-
- Matrix Template Library License
-
-
-
-
- Nara Institute of Science and Technology License (2003)
-
-
-
-
- ANTLR Software Rights Notice with license fallback
-
-
-
-
- PostgreSQL License
-
-
-
-
- Open Software License 1.0
-
-
-
-
- Nethack General Public License
-
-
-
-
- Creative Commons Attribution Non Commercial No Derivatives 4.0 International
-
-
-
-
- Code Project Open License 1.02
-
-
-
-
- FSF Unlimited License (with License Retention)
-
-
-
-
- GNU Free Documentation License v1.2 only - no invariants
-
-
-
-
- Net-SNMP License
-
-
-
-
- Amazon Digital Services License
-
-
-
-
- Sendmail License 8.23
-
-
-
-
- CNRI Jython License
-
-
-
-
- Reciprocal Public License 1.5
-
-
-
-
- BSD-2-Clause Plus Patent License
-
-
-
-
- SIL Open Font License 1.1 with no Reserved Font Name
-
-
-
-
- Apple Public Source License 1.2
-
-
-
-
- Open LDAP Public License v2.4
-
-
-
-
- Mozilla Public License 2.0 (no copyleft exception)
-
-
-
-
- ISC License
-
-
-
-
- Creative Commons Attribution Share Alike 2.5 Generic
-
-
-
-
- Sleepycat License
-
-
-
-
- CUA Office Public License v1.0
-
-
-
-
- Frameworx Open License 1.0
-
-
-
-
- Common Public Attribution License 1.0
-
-
-
-
- Norwegian Licence for Open Government Data (NLOD) 2.0
-
-
-
-
- Creative Commons Attribution Non Commercial 2.0 Generic
-
-
-
-
- GNU Free Documentation License v1.1 or later - no invariants
-
-
-
-
- Creative Commons Attribution 2.5 Generic
-
-
-
-
- Newsletr License
-
-
-
-
- The Parity Public License 7.0.0
-
-
-
-
- Leptonica License
-
-
-
-
- CMU License
-
-
-
-
- Adobe Postscript AFM License
-
-
-
-
- Creative Commons Attribution Non Commercial 2.5 Generic
-
-
-
-
- Cryptographic Autonomy License 1.0 (Combined Work Exception)
-
-
-
-
- BSD 4 Clause Shortened
-
-
-
-
- Netscape Public License v1.1
-
-
-
-
- Qhull License
-
-
-
-
- CeCILL-C Free Software License Agreement
-
-
-
-
- GNU General Public License v1.0 only
-
-
-
-
- Creative Commons Attribution Non Commercial No Derivatives 3.0 Germany
-
-
-
-
- Creative Commons Attribution Non Commercial Share Alike 3.0 Unported
-
-
-
-
- Creative Commons Attribution Non Commercial Share Alike 1.0 Generic
-
-
-
-
- MIT Open Group variant
-
-
-
-
- Multics License
-
-
-
-
- Scheme Widget Library (SWL) Software License Agreement
-
-
-
-
- GNU General Public License v1.0 or later
-
-
-
-
- GNU General Public License v3.0 or later
-
-
-
-
- DOC License
-
-
-
-
- PHP License v3.0
-
-
-
-
- Sun Industry Standards Source License v1.2
-
-
-
-
- Common Documentation License 1.0
-
-
-
-
- Lucent Public License Version 1.0
-
-
-
-
- Red Hat eCos Public License v1.1
-
-
-
-
- Licence Art Libre 1.3
-
-
-
-
- Creative Commons Attribution Share Alike 3.0 Germany
-
-
-
-
- Community Data License Agreement Permissive 1.0
-
-
-
-
- gnuplot License
-
-
-
-
- App::s2p License
-
-
-
-
- iMatix Standard Function Library Agreement
-
-
-
-
- Microsoft Public License
-
-
-
-
- eCos license version 2.0
-
-
-
-
- BSD 3-Clause "New" or "Revised" License
-
-
-
-
- Creative Commons Attribution Non Commercial No Derivatives 3.0 IGO
-
-
-
-
- ICU License
-
-
-
-
- GNU Affero General Public License v3.0 or later
-
-
-
-
- Creative Commons Attribution Share Alike 2.1 Japan
-
-
-
-
- Creative Commons Attribution Non Commercial Share Alike 4.0 International
-
-
-
-
- The Unlicense
-
-
-
-
- Creative Commons Attribution Non Commercial 3.0 Germany
-
-
-
-
- Open LDAP Public License v1.4
-
-
-
-
- CERN Open Hardware Licence Version 2 - Weakly Reciprocal
-
-
-
-
- SugarCRM Public License v1.1.3
-
-
-
-
- IPA Font License
-
-
-
-
- Academic Free License v2.0
-
-
-
-
- Unicode License Agreement - Data Files and Software (2016)
-
-
-
-
- Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported
-
-
-
-
- CERN Open Hardware Licence Version 2 - Permissive
-
-
-
-
- Creative Commons Attribution Non Commercial 3.0 Unported
-
-
-
-
- Copyfree Open Innovation License
-
-
-
-
- Cryptographic Autonomy License 1.0
-
-
-
-
- Licence Libre du Québec – Permissive version 1.1
-
-
-
-
- SIL Open Font License 1.1 with Reserved Font Name
-
-
-
-
- Lucent Public License v1.02
-
-
-
-
- Open LDAP Public License v1.3
-
-
-
-
- Taiwan Open Government Data License, version 1.0
-
-
-
-
- Creative Commons Attribution Non Commercial Share Alike 2.0 Generic
-
-
-
-
- Python License 2.0
-
-
-
-
- NTP No Attribution
-
-
-
-
- FSF All Permissive License
-
-
-
-
- Erlang Public License v1.1
-
-
-
-
- Barr License
-
-
-
-
- Creative Commons Attribution 3.0 United States
-
-
-
-
- BSD 3-Clause No Nuclear License 2014
-
-
-
-
- No Limit Public License
-
-
-
-
- BSD 3-Clause Clear License
-
-
-
-
- SGI Free Software License B v1.1
-
-
-
-
- Open Data Commons Public Domain Dedication & License 1.0
-
-
-
-
- Common Development and Distribution License 1.0
-
-
-
-
- GNU Lesser General Public License v2.1 or later
-
-
-
-
- Blue Oak Model License 1.0.0
-
-
-
-
- Creative Commons Attribution-NonCommercial-ShareAlike 2.0 France
-
-
-
-
- Fraunhofer FDK AAC Codec Library
-
-
-
-
- Standard ML of New Jersey License
-
-
-
-
- Affero General Public License v1.0 only
-
-
-
-
- CeCILL Free Software License Agreement v1.0
-
-
-
-
- Attribution Assurance License
-
-
-
-
- GNU General Public License v2.0 w/Font exception
-
-
-
-
- Info-ZIP License
-
-
-
-
- SSH OpenSSH license
-
-
-
-
- SSH short notice
-
-
-
-
- GNU General Public License v2.0 or later
-
-
-
-
- Clarified Artistic License
-
-
-
-
- SNIA Public License 1.1
-
-
-
-
- GNU Free Documentation License v1.1 only - invariants
-
-
-
-
- BSD 3-Clause No Military License
-
-
-
-
- GNU Free Documentation License v1.1
-
-
-
-
- Mozilla Public License 1.1
-
-
-
-
- Open LDAP Public License v1.1
-
-
-
-
- JSON License
-
-
-
-
- GNU Free Documentation License v1.3 only - no invariants
-
-
-
-
- OCLC Research Public License 2.0
-
-
-
-
- Open LDAP Public License v2.0.1
-
-
-
-
- FreeBSD Documentation License
-
-
-
-
- GNU General Public License v1.0 or later
-
-
-
-
- Yahoo! Public License v1.1
-
-
-
-
- Common Public License 1.0
-
-
-
-
- Apache License 1.0
-
-
-
-
- SIL Open Font License 1.0
-
-
-
-
- Creative Commons Attribution 4.0 International
-
-
-
-
- DSDP License
-
-
-
-
- IBM PowerPC Initialization and Boot Software
-
-
-
-
- MIT No Attribution
-
-
-
-
- Detection Rule License 1.0
-
-
-
-
- zlib License
-
-
-
-
- Adaptive Public License 1.0
-
-
-
-
- Sybase Open Watcom Public License 1.0
-
-
-
-
- GNU General Public License v2.0 w/GCC Runtime Library exception
-
-
-
-
- European Union Public License 1.2
-
-
-
-
- FSF Unlimited License
-
-
-
-
- NASA Open Source Agreement 1.3
-
-
-
-
- BSD 2-Clause "Simplified" License
-
-
-
-
- XFree86 License 1.1
-
-
-
-
- Eurosym License
-
-
-
-
- Open LDAP Public License v2.8
-
-
-
-
- dvipdfm License
-
-
-
-
- NIST Public Domain Notice
-
-
-
-
- Apache License 1.1
-
-
-
-
- The Parity Public License 6.0.0
-
-
-
-
- Creative Commons Attribution 2.0 Generic
-
-
-
-
- GNU Lesser General Public License v3.0 or later
-
-
-
-
- BSD 2-Clause with views sentence
-
-
-
-
- GNU General Public License v2.0 w/Classpath exception
-
-
-
-
- BSD 3-Clause No Nuclear Warranty
-
-
-
-
- X11 License
-
-
-
-
- Community Data License Agreement Permissive 2.0
-
-
-
-
- Haskell Language Report License
-
-
-
-
- Artistic License 1.0 w/clause 8
-
-
-
-
- Apple Public Source License 2.0
-
-
-
-
- GNU General Public License v3.0 or later
-
-
-
-
- Solderpad Hardware License v0.5
-
-
-
-
- CNRI Python Open Source GPL Compatible License Agreement
-
-
-
-
- Condor Public License v1.1
-
-
-
-
- Open LDAP Public License v2.3
-
-
-
-
- GNU General Public License v2.0 only
-
-
-
-
- Business Source License 1.1
-
-
-
-
- Licence Libre du Québec – Réciprocité version 1.1
-
-
-
-
- Academy of Motion Picture Arts and Sciences BSD
-
-
-
-
- copyleft-next 0.3.1
-
-
-
-
- GNU Free Documentation License v1.3 or later - invariants
-
-
-
-
- Open LDAP Public License v2.7
-
-
-
-
- Open Software License 2.0
-
-
-
-
- Unicode License Agreement - Data Files and Software (2015)
-
-
-
-
- Computer Associates Trusted Open Source License 1.1
-
-
-
-
- Ricoh Source Code Public License
-
-
-
-
- PNG Reference Library version 2
-
-
-
-
- LaTeX Project Public License v1.1
-
-
-
-
- Community Data License Agreement Sharing 1.0
-
-
-
-
- Glulxe License
-
-
-
-
- GNU Free Documentation License v1.3 or later - no invariants
-
-
-
-
- Open LDAP Public License v1.2
-
-
-
-
- Common Development and Distribution License 1.1
-
-
-
-
- CERN Open Hardware Licence v1.1
-
-
-
-
- BSD Source Code Attribution
-
-
-
-
- Independent JPEG Group License
-
-
-
-
- Zimbra Public License v1.4
-
-
-
-
- BSD Zero Clause License
-
-
-
-
- Creative Commons Attribution 1.0 Generic
-
-
-
-
- wxWindows Library License
-
-
-
-
- Zope Public License 2.1
-
-
-
-
- NTP License
-
-
-
-
- Artistic License 1.0 (Perl)
-
-
-
-
- Creative Commons Attribution No Derivatives 2.0 Generic
-
-
-
-
- Creative Commons Attribution No Derivatives 4.0 International
-
-
-
-
- Adobe Systems Incorporated Source Code License Agreement
-
-
-
-
- Eclipse Public License 1.0
-
-
-
-
- diffmark license
-
-
-
-
- xinetd License
-
-
-
-
- Plexus Classworlds License
-
-
-
-
- Japan Network Information Center License
-
-
-
-
- Adobe Glyph List License
-
-
-
-
- Cube License
-
-
-
-
- TCP Wrappers License
-
-
-
-
- Creative Commons Attribution Share Alike 1.0 Generic
-
-
-
-
- BSD 2-Clause FreeBSD License
-
-
-
-
- Open Government Licence - Canada
-
-
-
-
- ANTLR Software Rights Notice
-
-
-
-
- GNU Library General Public License v2.1 or later
-
-
-
-
- Open Software License 2.1
-
-
-
-
- psutils License
-
-
-
-
- SCEA Shared Source License
-
-
-
-
- The MirOS Licence
-
-
-
-
- Hippocratic License 2.1
-
-
-
-
- GNU Free Documentation License v1.2 only - invariants
-
-
-
-
- GNU Lesser General Public License v2.1 only
-
-
-
-
- Entessa Public License v1.0
-
-
-
-
- Microsoft Reciprocal License
-
-
-
-
- libselinux public domain notice
-
-
-
-
- GNU Library General Public License v2 only
-
-
-
-
- Open LDAP Public License v2.5
-
-
-
-
- Imlib2 License
-
-
-
-
- libpng License
-
-
-
-
- Scheme Language Report License
-
-
-
-
- Mozilla Public License 1.0
-
-
-
-
- Sax Public Domain Notice
-
-
-
-
- Norwegian Licence for Open Government Data (NLOD) 1.0
-
-
-
-
- Simple Public License 2.0
-
-
-
-
- Technische Universitaet Berlin License 1.0
-
-
-
-
- GNU Free Documentation License v1.1 only - no invariants
-
-
-
-
- Creative Commons Attribution No Derivatives 3.0 Germany
-
-
-
-
- MakeIndex License
-
-
-
-
- EPICS Open License
-
-
-
-
- GNU Free Documentation License v1.3 only - invariants
-
-
-
-
- XSkat License
-
-
-
-
- bzip2 and libbzip2 License v1.0.5
-
-
-
-
- Community Specification License 1.0
-
-
-
-
- GL2PS License
-
-
-
-
- Historical Permission Notice and Disclaimer
-
-
-
-
- bzip2 and libbzip2 License v1.0.6
-
-
-
-
- Creative Commons Attribution Non Commercial 1.0 Generic
-
-
-
-
- Fair License
-
-
-
-
- CeCILL-B Free Software License Agreement
-
-
-
-
- 3dfx Glide License
-
-
-
-
- Creative Commons Attribution Share Alike 4.0 International
-
-
-
-
- Creative Commons Zero v1.0 Universal
-
-
-
-
- enna License
-
-
-
-
- Wsuipa License
-
-
-
-
- RSA Message-Digest License
-
-
-
-
- VOSTROM Public License for Open Source
-
-
-
-
- Open Use of Data Agreement v1.0
-
-
-
-
- CERN Open Hardware Licence Version 2 - Strongly Reciprocal
-
-
-
-
- X11 License Distribution Modification Variant
-
-
-
-
- copyleft-next 0.3.0
-
-
-
-
- Zimbra Public License v1.3
-
-
-
-
- NIST Public Domain Notice with license fallback
-
-
-
-
- Nokia Open Source License
-
-
-
-
- Academic Free License v2.1
-
-
-
-
- Zope Public License 2.0
-
-
-
-
- Open Data Commons Open Database License v1.0
-
-
-
-
- zlib/libpng License with Acknowledgement
-
-
-
-
- PHP License v3.01
-
-
-
-
- Afmparse License
-
-
-
-
- Historical Permission Notice and Disclaimer - sell variant
-
-
-
-
- PolyForm Small Business License 1.0.0
-
-
-
-
- IBM Public License v1.0
-
-
-
-
- CeCILL Free Software License Agreement v1.1
-
-
-
-
- feh License
-
-
-
-
- SIL Open Font License 1.0 with Reserved Font Name
-
-
-
-
- TMate Open Source License
-
-
-
-
- BSD 3-Clause No Nuclear License
-
-
-
-
- W3C Software Notice and License (1998-07-20)
-
-
-
-
- Sun Public License v1.0
-
-
-
-
- NetCDF license
-
-
-
-
- Aladdin Free Public License
-
-
-
-
- AMD's plpa_map.c License
-
-
-
-
- CrystalStacker License
-
-
-
-
- Intel ACPI Software License Agreement
-
-
-
-
- CERN Open Hardware Licence v1.2
-
-
-
-
- Creative Commons Attribution Non Commercial Share Alike 3.0 Germany
-
-
-
-
- MIT License
-
-
-
-
- Zed License
-
-
-
-
- Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B)
-
-
-
-
- Mulan Permissive Software License, Version 1
-
-
-
-
- Eiffel Forum License v2.0
-
-
-
-
- Latex2e License
-
-
-
-
- Spencer License 94
-
-
-
-
- Open Public License v1.0
-
-
-
-
- Creative Commons Attribution Non Commercial 4.0 International
-
-
-
-
- GNU Lesser General Public License v3.0 or later
-
-
-
-
- Universal Permissive License v1.0
-
-
-
-
- University of Illinois/NCSA Open Source License
-
-
-
-
- SGI Free Software License B v2.0
-
-
-
-
- GNU General Public License v3.0 w/GCC Runtime Library exception
-
-
-
-
- Zend License v2.0
-
-
-
-
- ImageMagick License
-
-
-
-
- Open LDAP Public License v2.6
-
-
-
-
- Unicode Terms of Use
-
-
-
-
- GNU General Public License v3.0 only
-
-
-
-
- Artistic License 2.0
-
-
-
-
- SQLite Blessing
-
-
-
-
- Etalab Open License 2.0
-
-
-
-
- GNU Free Documentation License v1.2 only
-
-
-
-
- LaTeX Project Public License v1.0
-
-
-
-
- Rdisc License
-
-
-
-
- BSD 3-Clause Modification
-
-
-
-
- Xerox License
-
-
-
-
- Mozilla Public License 2.0
-
-
-
-
- BitTorrent Open Source License v1.1
-
-
-
-
- Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic
-
-
-
-
- Sun Industry Standards Source License v1.1
-
-
-
-
- libtiff License
-
-
-
-
- Creative Commons Attribution Non Commercial Share Alike 2.0 England and Wales
-
-
-
-
- Deutsche Freie Software Lizenz
-
-
-
-
- LaTeX Project Public License v1.2
-
-
-
-
- TAPR Open Hardware License v1.0
-
-
-
-
- European Union Public License 1.0
-
-
-
-
- Solderpad Hardware License, Version 0.51
-
-
-
-
- Freetype Project License
-
-
-
-
- W3C Software Notice and Document License (2015-05-13)
-
-
-
-
- OSET Public License version 2.1
-
-
-
-
- EU DataGrid Software License
-
-
-
-
- Upstream Compatibility License v1.0
-
-
-
-
- Borceux license
-
-
-
-
- Elastic License 2.0
-
-
-
-
- BSD 2-Clause NetBSD License
-
-
-
-
- BSD 3-Clause Open MPI variant
-
-
-
-
- Open Software License 3.0
-
-
-
-
- curl License
-
-
-
-
- Spencer License 86
-
-
-
-
- Boost Software License 1.0
-
-
-
-
- Standard ML of New Jersey License
-
-
-
-
- Trusster Open Source License
-
-
-
-
- Netizen Open Source License
-
-
-
-
- Academic Free License v1.2
-
-
-
-
- Mulan Permissive Software License, Version 2
-
-
-
-
- Motosoto License
-
-
-
-
- Creative Commons Attribution Non Commercial Share Alike 2.5 Generic
-
-
-
-
- JasPer License
-
-
-
-
- BSD-4-Clause (University of California-Specific)
-
-
-
-
- Bahyph License
-
-
-
-
- Vovida Software License v1.0
-
-
-
-
- W3C Software Notice and License (2002-12-31)
-
-
-
-
- Open Data Commons Attribution License v1.0
-
-
-
-
- BitTorrent Open Source License v1.0
-
-
-
-
- Open Government Licence v2.0
-
-
-
-
- GNU Lesser General Public License v3.0 only
-
-
-
-
- X.Net License
-
-
-
-
- Ruby License
-
-
-
-
- GNU Free Documentation License v1.3
-
-
-
-
- Zope Public License 1.1
-
-
-
-
- Open CASCADE Technology Public License
-
-
-
-
- LaTeX Project Public License v1.3c
-
-
-
-
- Apache License 2.0
-
-
-
-
- GD License
-
-
-
-
- Creative Commons Attribution 3.0 Netherlands
-
-
-
-
- LaTeX Project Public License v1.3a
-
-
-
-
- Creative Commons Attribution 2.5 Australia
-
-
-
-
- GNU Free Documentation License v1.1 only
-
-
-
-
- GNU Free Documentation License v1.1 or later
-
-
-
-
- Open Government Licence v3.0
-
-
-
-
- Yahoo! Public License v1.0
-
-
-
-
- Reciprocal Public License 1.1
-
-
-
-
- GNU Library General Public License v2 or later
-
-
-
-
- Open Publication License v1.0
-
-
-
-
- Noweb License
-
-
-
-
- Academic Free License v3.0
-
-
-
-
- Nunit License
-
-
-
-
- Creative Commons Attribution 3.0 Unported
-
-
-
-
- Beerware License
-
-
-
-
- Caldera License
-
-
-
-
- GNU General Public License v1.0 only
-
-
-
-
- GNU General Public License v2.0 or later
-
-
-
-
- Non-Commercial Government Licence
-
-
-
-
- Creative Commons Attribution No Derivatives 2.5 Generic
-
-
-
-
- GNU General Public License v2.0 only
-
-
-
-
- Intel Open Source License
-
-
-
-
- Vim License
-
-
-
-
- Creative Commons Attribution Share Alike 2.0 Generic
-
-
-
-
- MIT +no-false-attribs license
-
-
-
-
- Apple Public Source License 1.1
-
-
-
-
- GNU Free Documentation License v1.2 or later
-
-
-
-
- BSD with attribution
-
-
-
-
- SIL Open Font License 1.0 with no Reserved Font Name
-
-
-
-
- Naumen Public License
-
-
-
-
- Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic
-
-
-
-
- Computational Use of Data Agreement v1.0
-
-
-
-
- Lesser General Public License For Linguistic Resources
-
-
-
-
- mpich2 License
-
-
-
-
- Apple Public Source License 1.0
-
-
-
-
- Linux Kernel Variant of OpenIB.org license
-
-
-
-
- Enlightenment License (e16)
-
-
-
-
- GNU Free Documentation License v1.2
-
-
-
-
- Open Group Test Suite License
-
-
-
-
- Dotseqn License
-
-
-
-
- Data licence Germany – attribution – version 2.0
-
-
-
-
- Saxpath License
-
-
-
-
- GNU Affero General Public License v3.0
-
-
-
-
- Abstyles License
-
-
-
-
- Creative Commons Attribution Share Alike 3.0 Unported
-
-
-
-
- Giftware License
-
-
-
-
- FreeImage Public License v1.0
-
-
-
-
- CeCILL Free Software License Agreement v2.1
-
-
-
-
- RealNetworks Public Source License v1.0
-
-
-
-
- GNU Free Documentation License v1.3 or later
-
-
-
-
- GNU Free Documentation License v1.1 or later - invariants
-
-
-
-
- Educational Community License v2.0
-
-
-
-
- Licence Libre du Québec – Réciprocité forte version 1.1
-
-
-
-
- GNU General Public License v3.0 w/Autoconf exception
-
-
-
-
- Jam License
-
-
-
-
- GNU Free Documentation License v1.2 or later - no invariants
-
-
-
-
- CeCILL Free Software License Agreement v2.0
-
-
-
-
- PolyForm Noncommercial License 1.0.0
-
-
-
-
- OGC Software License, Version 1.0
-
-
-
-
- Creative Commons Attribution No Derivatives 3.0 Unported
-
-
-
-
- Q Public License 1.0
-
-
-
-
- Licence Art Libre 1.2
-
-
-
-
- Creative Commons Attribution 3.0 Germany
-
-
-
-
- OpenSSL License
-
-
-
-
- Spencer License 99
-
-
-
-
- Creative Commons Attribution Share Alike 3.0 Austria
-
-
-
-
- BSD Protection License
-
-
-
-
- Open LDAP Public License 2.2.2
-
-
-
-
- NRL License
-
-
-
-
- TORQUE v2.5+ Software License v1.1
-
-
-
-
- HTML Tidy License
-
-
-
-
- Server Side Public License, v 1
-
-
-
-
- Netscape Public License v1.0
-
-
-
-
- GNU Library General Public License v2 only
-
-
-
-
- GNU Affero General Public License v3.0 only
-
-
-
-
- GNU Free Documentation License v1.2 or later - invariants
-
-
-
-
- GNU General Public License v2.0 w/Bison exception
-
-
-
-
- Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic
-
-
-
-
- Educational Community License v1.0
-
-
-
-
- Do What The F*ck You Want To Public License
-
-
-
-
- Creative Commons Attribution Share Alike 2.0 England and Wales
-
-
-
-
- GNU General Public License v3.0 only
-
-
-
-
- Open LDAP Public License v2.2.1
-
-
-
-
- Secure Messaging Protocol Public License
-
-
-
-
- Creative Commons Attribution 3.0 Austria
-
-
-
-
- Eiffel Forum License v1.0
-
-
-
-
- Net Boolean Public License v1
-
-
-
-
- Lawrence Berkeley National Labs BSD variant license
-
-
-
-
- Affero General Public License v1.0
-
-
-
-
- Crossword License
-
-
-
-
- TCL/TK License
-
-
-
-
- Creative Commons Attribution No Derivatives 1.0 Generic
-
-
-
-
- Apple MIT License
-
-
-
-
- Technische Universitaet Berlin License 2.0
-
-
-
-
- GNU Free Documentation License v1.3 only
-
-
-
-
- Non-Profit Open Software License 3.0
-
-
-
-
- BSD 4-Clause "Original" or "Old" License
-
-
-
-
- gSOAP Public License v1.3b
-
-
-
-
- GNU Lesser General Public License v2.1 only
-
-
-
-
- GNU Lesser General Public License v3.0 only
-
-
-
-
-
- FreeRTOS Exception 2.0
-
-
-
-
- Swift Exception
-
-
-
-
- Qt LGPL exception 1.1
-
-
-
-
- GNU JavaMail exception
-
-
-
-
- CLISP exception 2.0
-
-
-
-
- eCos exception 2.0
-
-
-
-
- GPL Cooperation Commitment 1.0
-
-
-
-
- DigiRule FOSS License Exception
-
-
-
-
- Font exception 2.0
-
-
-
-
- Qt GPL exception 1.0
-
-
-
-
- PS/PDF font exception (2017-08-17)
-
-
-
-
- GPL-3.0 Linking Exception (with Corresponding Source)
-
-
-
-
- Linux Syscall Note
-
-
-
-
- GCC Runtime Library exception 2.0
-
-
-
-
- LZMA exception
-
-
-
-
- Autoconf exception 3.0
-
-
-
-
- U-Boot exception 2.0
-
-
-
-
- LLVM Exception
-
-
-
-
- OCaml LGPL Linking Exception
-
-
-
-
- Autoconf exception 2.0
-
-
-
-
- Bootloader Distribution Exception
-
-
-
-
- LGPL-3.0 Linking Exception
-
-
-
-
- OpenVPN OpenSSL Exception
-
-
-
-
- FLTK exception
-
-
-
-
- Bison exception 2.2
-
-
-
-
- Open CASCADE Exception 1.0
-
-
-
-
- GCC Runtime Library exception 3.1
-
-
-
-
- OpenJDK Assembly exception 1.0
-
-
-
-
- WxWindows Library Exception 3.1
-
-
-
-
- Fawkes Runtime Exception
-
-
-
-
- Nokia Qt LGPL exception 1.1
-
-
-
-
- Qwt exception 1.0
-
-
-
-
- Universal FOSS Exception, Version 1.0
-
-
-
-
- Classpath exception 2.0
-
-
-
-
- Solderpad Hardware License v2.0
-
-
-
-
- GPL-3.0 Linking Exception
-
-
-
-
- Solderpad Hardware License v2.1
-
-
-
-
- Libtool Exception
-
-
-
-
- Macros and Inline Functions Exception
-
-
-
-
- 389 Directory Server Exception
-
-
-
-
- i2p GPL+Java Exception
-
-
-
-
-
-
diff --git a/schema/grype/db-search-vuln/json/README.md b/schema/grype/db-search-vuln/json/README.md
new file mode 100644
index 00000000000..844ff09fa4c
--- /dev/null
+++ b/schema/grype/db-search-vuln/json/README.md
@@ -0,0 +1,28 @@
+# `db-search vuln` JSON Schema
+
+This is the JSON schema for output from the `grype db search vuln` command. The required inputs for defining the JSON schema are as follows:
+
+- the value of `cmd/grype/cli/commands/internal/dbsearch.VulnerabilitiesSchemaVersion` that governs the schema version
+- the `Vulnerabilities` type definition within `github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go` that governs the overall document shape
+
+## Versioning
+
+Versioning the JSON schema must be done manually by changing the `VulnerabilitiesSchemaVersion` constant within `cmd/grype/cli/commands/internal/dbsearch/versions.go`.
+
+This schema is being versioned based off of the "SchemaVer" guidelines, which slightly diverges from Semantic Versioning to tailor for the purposes of data models.
+
+Given a version number format `MODEL.REVISION.ADDITION`:
+
+- `MODEL`: increment when you make a breaking schema change which will prevent interaction with any historical data
+- `REVISION`: increment when you make a schema change which may prevent interaction with some historical data
+- `ADDITION`: increment when you make a schema change that is compatible with all historical data
+
+## Generating a New Schema
+
+Create the new schema by running `make generate-json-schema` from the root of the repo:
+
+- If there is **not** an existing schema for the given version, then the new schema file will be written to `schema/grype/db-search-vuln/json/schema-$VERSION.json`
+- If there is an existing schema for the given version and the new schema matches the existing schema, no action is taken
+- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section)
+
+***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version.
diff --git a/schema/grype/db-search-vuln/json/schema-1.0.0.json b/schema/grype/db-search-vuln/json/schema-1.0.0.json
new file mode 100644
index 00000000000..589230789dc
--- /dev/null
+++ b/schema/grype/db-search-vuln/json/schema-1.0.0.json
@@ -0,0 +1,164 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "anchore.io/schema/grype/db-search-vuln/json/1.0.0/vulnerabilities",
+ "$ref": "#/$defs/Vulnerabilities",
+ "$defs": {
+ "OperatingSystem": {
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "version"
+ ]
+ },
+ "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"
+ ]
+ },
+ "Vulnerabilities": {
+ "items": {
+ "$ref": "#/$defs/Vulnerability"
+ },
+ "type": "array"
+ },
+ "Vulnerability": {
+ "$defs": {
+ "affected_packages": {
+ "description": "is the number of packages affected by the vulnerability"
+ },
+ "operating_systems": {
+ "description": "is a list of operating systems affected by the vulnerability"
+ }
+ },
+ "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"
+ },
+ "operating_systems": {
+ "items": {
+ "$ref": "#/$defs/OperatingSystem"
+ },
+ "type": "array"
+ },
+ "affected_packages": {
+ "type": "integer"
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "provider",
+ "status",
+ "operating_systems",
+ "affected_packages"
+ ]
+ }
+ }
+}
diff --git a/schema/grype/db-search-vuln/json/schema-1.0.1.json b/schema/grype/db-search-vuln/json/schema-1.0.1.json
new file mode 100644
index 00000000000..bea5a4069cf
--- /dev/null
+++ b/schema/grype/db-search-vuln/json/schema-1.0.1.json
@@ -0,0 +1,244 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "anchore.io/schema/grype/db-search-vuln/json/1.0.1/vulnerabilities",
+ "$ref": "#/$defs/Vulnerabilities",
+ "$defs": {
+ "EPSS": {
+ "properties": {
+ "cve": {
+ "type": "string"
+ },
+ "epss": {
+ "type": "number"
+ },
+ "percentile": {
+ "type": "number"
+ },
+ "date": {
+ "type": "string"
+ }
+ },
+ "type": "object",
+ "required": [
+ "cve",
+ "epss",
+ "percentile",
+ "date"
+ ]
+ },
+ "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"
+ ]
+ },
+ "OperatingSystem": {
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "version"
+ ]
+ },
+ "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"
+ ]
+ },
+ "Vulnerabilities": {
+ "items": {
+ "$ref": "#/$defs/Vulnerability"
+ },
+ "type": "array"
+ },
+ "Vulnerability": {
+ "$defs": {
+ "affected_packages": {
+ "description": "is the number of packages affected by the vulnerability"
+ },
+ "operating_systems": {
+ "description": "is a list of operating systems affected by the vulnerability"
+ }
+ },
+ "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"
+ },
+ "operating_systems": {
+ "items": {
+ "$ref": "#/$defs/OperatingSystem"
+ },
+ "type": "array"
+ },
+ "affected_packages": {
+ "type": "integer"
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "provider",
+ "status",
+ "operating_systems",
+ "affected_packages"
+ ]
+ }
+ }
+}
diff --git a/schema/grype/db-search-vuln/json/schema-latest.json b/schema/grype/db-search-vuln/json/schema-latest.json
new file mode 100644
index 00000000000..bea5a4069cf
--- /dev/null
+++ b/schema/grype/db-search-vuln/json/schema-latest.json
@@ -0,0 +1,244 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "anchore.io/schema/grype/db-search-vuln/json/1.0.1/vulnerabilities",
+ "$ref": "#/$defs/Vulnerabilities",
+ "$defs": {
+ "EPSS": {
+ "properties": {
+ "cve": {
+ "type": "string"
+ },
+ "epss": {
+ "type": "number"
+ },
+ "percentile": {
+ "type": "number"
+ },
+ "date": {
+ "type": "string"
+ }
+ },
+ "type": "object",
+ "required": [
+ "cve",
+ "epss",
+ "percentile",
+ "date"
+ ]
+ },
+ "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"
+ ]
+ },
+ "OperatingSystem": {
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "version"
+ ]
+ },
+ "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"
+ ]
+ },
+ "Vulnerabilities": {
+ "items": {
+ "$ref": "#/$defs/Vulnerability"
+ },
+ "type": "array"
+ },
+ "Vulnerability": {
+ "$defs": {
+ "affected_packages": {
+ "description": "is the number of packages affected by the vulnerability"
+ },
+ "operating_systems": {
+ "description": "is a list of operating systems affected by the vulnerability"
+ }
+ },
+ "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"
+ },
+ "operating_systems": {
+ "items": {
+ "$ref": "#/$defs/OperatingSystem"
+ },
+ "type": "array"
+ },
+ "affected_packages": {
+ "type": "integer"
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "provider",
+ "status",
+ "operating_systems",
+ "affected_packages"
+ ]
+ }
+ }
+}
diff --git a/schema/grype/db-search/json/README.md b/schema/grype/db-search/json/README.md
new file mode 100644
index 00000000000..6ee954c4790
--- /dev/null
+++ b/schema/grype/db-search/json/README.md
@@ -0,0 +1,28 @@
+# `db-search` JSON Schema
+
+This is the JSON schema for output from the `grype db search` command. The required inputs for defining the JSON schema are as follows:
+
+- the value of `cmd/grype/cli/commands/internal/dbsearch.MatchesSchemaVersion` that governs the schema version
+- the `Matches` type definition within `github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch/matches.go` that governs the overall document shape
+
+## Versioning
+
+Versioning the JSON schema must be done manually by changing the `MatchesSchemaVersion` constant within `cmd/grype/cli/commands/internal/dbsearch/versions.go`.
+
+This schema is being versioned based off of the "SchemaVer" guidelines, which slightly diverges from Semantic Versioning to tailor for the purposes of data models.
+
+Given a version number format `MODEL.REVISION.ADDITION`:
+
+- `MODEL`: increment when you make a breaking schema change which will prevent interaction with any historical data
+- `REVISION`: increment when you make a schema change which may prevent interaction with some historical data
+- `ADDITION`: increment when you make a schema change that is compatible with all historical data
+
+## Generating a New Schema
+
+Create the new schema by running `make generate-json-schema` from the root of the repo:
+
+- If there is **not** an existing schema for the given version, then the new schema file will be written to `schema/grype/db-search/json/schema-$VERSION.json`
+- If there is an existing schema for the given version and the new schema matches the existing schema, no action is taken
+- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section)
+
+***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version.
diff --git a/schema/grype/db-search/json/schema-1.0.0.json b/schema/grype/db-search/json/schema-1.0.0.json
new file mode 100644
index 00000000000..6f3eef64542
--- /dev/null
+++ b/schema/grype/db-search/json/schema-1.0.0.json
@@ -0,0 +1,436 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "anchore.io/schema/grype/db-search/json/1.0.0/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"
+ },
+ "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"
+ },
+ "detail": {
+ "$ref": "#/$defs/AffectedPackageBlob"
+ }
+ },
+ "type": "object",
+ "required": [
+ "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"
+ ]
+ },
+ "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"
+ },
+ "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": {
+ "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"
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "provider",
+ "status"
+ ]
+ }
+ }
+}
diff --git a/schema/grype/db-search/json/schema-1.0.1.json b/schema/grype/db-search/json/schema-1.0.1.json
new file mode 100644
index 00000000000..d1ddb779dd3
--- /dev/null
+++ b/schema/grype/db-search/json/schema-1.0.1.json
@@ -0,0 +1,522 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "anchore.io/schema/grype/db-search/json/1.0.1/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"
+ },
+ "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"
+ },
+ "detail": {
+ "$ref": "#/$defs/AffectedPackageBlob"
+ }
+ },
+ "type": "object",
+ "required": [
+ "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-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
new file mode 100644
index 00000000000..365c6583310
--- /dev/null
+++ b/schema/grype/db-search/json/schema-latest.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/templates/README.md b/templates/README.md
index a9f521af0a7..80e8097b061 100644
--- a/templates/README.md
+++ b/templates/README.md
@@ -9,6 +9,9 @@ Current templates:
.
├── README.md
+├── html.tmpl
+├── junit.tmpl
+├── csv.tmpl
└── table.tmpl
@@ -21,3 +24,22 @@ This template mimics the "default" table output of Grype, there are some drawbac
- no (wont-fix) logic
As you can see from the above list, it's not perfect but it's a start.
+
+## HTML
+
+Produces a nice html template with a dynamic table using datatables.js.
+
+You can also modify the templating filter to limit the output to a subset.
+
+Default includes all
+
+```
+ {{- 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") }}
+```
+
+We can limit it to only Critical, High, and Medium by editing the filter as follows
+
+```
+ {{- if or (eq $vuln.Vulnerability.Severity "Critical") (eq $vuln.Vulnerability.Severity "High") (eq $vuln.Vulnerability.Severity "Medium") }}
+```
+
diff --git a/templates/html.tmpl b/templates/html.tmpl
new file mode 100644
index 00000000000..a550829375d
--- /dev/null
+++ b/templates/html.tmpl
@@ -0,0 +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 }}
+
+
+
+
+
+
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}}
+
+
+
+
+
+
+
+
+
+
+
Critical
+
{{ $CountCritical }}
+
+
+
High
+
{{ $CountHigh }}
+
+
+
Medium
+
{{ $CountMedium }}
+
+
+
Low
+
{{ $CountLow }}
+
+
+
Unknown
+
{{ $CountUnknown }}
+
+
+
+
+
+
+ Name
+ Version
+ Type
+ Vulnerability
+ Severity
+ State
+ Fixed In
+ Description
+ Related URLs
+ PURL
+
+
+
+ {{- range $FilteredMatches }}
+
+ {{.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 }}
+
+ {{- end }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/junit.tmpl b/templates/junit.tmpl
new file mode 100644
index 00000000000..71b7c692e3a
--- /dev/null
+++ b/templates/junit.tmpl
@@ -0,0 +1,14 @@
+
+
+{{- $failures := len $.Matches }}
+
+
+
+
+ {{- range .Matches }}
+
+ {{html .Vulnerability.Description }} {{ .Artifact.CPEs }} {{ .Vulnerability.DataSource }}
+
+ {{- end }}
+
+
diff --git a/test/cli/cmd_test.go b/test/cli/cmd_test.go
index ad9f79303a0..62e37407a89 100644
--- a/test/cli/cmd_test.go
+++ b/test/cli/cmd_test.go
@@ -1,8 +1,12 @@
package cli
import (
+ "encoding/json"
+ "path/filepath"
"strings"
"testing"
+
+ "github.com/stretchr/testify/require"
)
func TestCmd(t *testing.T) {
@@ -21,13 +25,22 @@ func TestCmd(t *testing.T) {
assertFailingReturnCode,
},
},
+ {
+ name: "empty-string-arg-shows-help",
+ args: []string{""},
+ assertions: []traitAssertion{
+ assertInOutput("an image/directory argument is required"), // specific error that should be shown
+ assertInOutput("A vulnerability scanner for container images, filesystems, and SBOMs"), // excerpt from help description
+ assertFailingReturnCode,
+ },
+ },
{
name: "ensure valid descriptor",
args: []string{getFixtureImage(t, "image-bare"), "-o", "json"},
assertions: []traitAssertion{
- assertInOutput(`"check-for-app-update": false`), // assert existence of the app config block
- assertInOutput(`"db": {`), // assert existence of the db status block
- assertInOutput(`"built":`), // assert existence of the db status block
+ assertInOutput(`"check-for-app-update":`), // assert existence of the app config block
+ assertInOutput(`"db":`), // assert existence of the db status block
+ assertInOutput(`"built":`), // assert existence of the db status block
},
},
{
@@ -37,21 +50,78 @@ func TestCmd(t *testing.T) {
assertInOutput("sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853"), // linux/arm64 image digest
},
},
+ // TODO: uncomment this test when we can use `grype config`
+ //{
+ // name: "responds-to-search-options",
+ // args: []string{"--help"},
+ // env: map[string]string{
+ // "GRYPE_SEARCH_UNINDEXED_ARCHIVES": "true",
+ // "GRYPE_SEARCH_INDEXED_ARCHIVES": "false",
+ // "GRYPE_SEARCH_SCOPE": "all-layers",
+ // },
+ // assertions: []traitAssertion{
+ // // the application config in the log matches that of what we expect to have been configured. Note:
+ // // we are not testing further wiring of this option, only that the config responds to
+ // // package-cataloger-level options.
+ // assertInOutput("unindexed-archives: true"),
+ // assertInOutput("indexed-archives: false"),
+ // assertInOutput("scope: 'all-layers'"),
+ // },
+ //},
+ {
+ name: "vulnerabilities in output on -f with failure",
+ args: []string{"registry:busybox:1.31", "-f", "high", "--platform", "linux/amd64"},
+ assertions: []traitAssertion{
+ assertInOutput("CVE-2021-42379"),
+ assertFailingReturnCode,
+ },
+ },
{
- name: "responds-to-search-options",
- args: []string{"-vv"},
- env: map[string]string{
- "GRYPE_SEARCH_UNINDEXED_ARCHIVES": "true",
- "GRYPE_SEARCH_INDEXED_ARCHIVES": "false",
- "GRYPE_SEARCH_SCOPE": "all-layers",
+ name: "reason for ignored vulnerabilities is available in the template",
+ args: []string{
+ "sbom:" + filepath.Join("test-fixtures", "test-ignore-reason", "sbom.json"),
+ "-c", filepath.Join("test-fixtures", "test-ignore-reason", "config-with-ignore.yaml"),
+ "-o", "template",
+ "-t", filepath.Join("test-fixtures", "test-ignore-reason", "template-with-ignore-reasons"),
},
assertions: []traitAssertion{
- // the application config in the log matches that of what we expect to have been configured. Note:
- // we are not testing further wiring of this option, only that the config responds to
- // package-cataloger-level options.
- assertInOutput("unindexed-archives: true"),
- assertInOutput("indexed-archives: false"),
- assertInOutput("scope: all-layers"),
+ assertInOutput("CVE-2021-42385 (test reason for vulnerability being ignored)"),
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "ignore-states wired up",
+ args: []string{"./test-fixtures/sbom-grype-source.json", "--ignore-states", "unknown"},
+ assertions: []traitAssertion{
+ assertSucceedingReturnCode,
+ assertRowInStdOut([]string{"Pygments", "2.6.1", "2.7.4", "python", "GHSA-pq64-v7f5-gqh8", "High"}),
+ assertNotInOutput("CVE-2014-6052"),
+ },
+ },
+ {
+ name: "ignore-states wired up - ignore fixed",
+ args: []string{"./test-fixtures/sbom-grype-source.json", "--ignore-states", "fixed"},
+ assertions: []traitAssertion{
+ assertSucceedingReturnCode,
+ assertRowInStdOut([]string{"libvncserver", "0.9.9", "apk", "CVE-2014-6052", "High"}),
+ assertNotInOutput("GHSA-pq64-v7f5-gqh8"),
+ },
+ },
+ {
+ name: "ignore-states wired up - ignore fixed, show suppressed",
+ args: []string{"./test-fixtures/sbom-grype-source.json", "--ignore-states", "fixed", "--show-suppressed"},
+ assertions: []traitAssertion{
+ assertSucceedingReturnCode,
+ assertRowInStdOut([]string{"Pygments", "2.6.1", "2.7.4", "python", "GHSA-pq64-v7f5-gqh8", "High", "(suppressed)"}),
+ },
+ },
+ {
+ // from: https://github.com/anchore/grype/issues/2412 we need to ensure that explicit ignores in code don't break
+ name: "explicit ignores wired up",
+ args: []string{getFixtureImage(t, "image-java-subprocess")},
+ assertions: []traitAssertion{
+ assertSucceedingReturnCode,
+ assertNotInOutput("CVE-2023-45853"),
},
},
}
@@ -70,3 +140,20 @@ func TestCmd(t *testing.T) {
})
}
}
+
+func Test_descriptorNameAndVersionSet(t *testing.T) {
+ _, output, _ := runGrype(t, nil, "-o", "json", getFixtureImage(t, "image-bare"))
+
+ parsed := map[string]any{}
+ err := json.Unmarshal([]byte(output), &parsed)
+ require.NoError(t, err)
+
+ desc, _ := parsed["descriptor"].(map[string]any)
+ require.NotNil(t, desc)
+
+ name := desc["name"]
+ require.Equal(t, "grype", name)
+
+ version := desc["version"]
+ require.NotEmpty(t, version)
+}
diff --git a/test/cli/config_test.go b/test/cli/config_test.go
new file mode 100644
index 00000000000..0f0eaca58ea
--- /dev/null
+++ b/test/cli/config_test.go
@@ -0,0 +1,171 @@
+package cli
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "gopkg.in/yaml.v3"
+)
+
+func Test_configLoading(t *testing.T) {
+ cwd, err := os.Getwd()
+ require.NoError(t, err)
+ defer func() { require.NoError(t, os.Chdir(cwd)) }()
+
+ configsDir := filepath.Join(cwd, "test-fixtures", "configs")
+ path := func(path string) string {
+ return filepath.Join(configsDir, filepath.Join(strings.Split(path, "/")...))
+ }
+
+ type ignore struct {
+ Vuln string `yaml:"vulnerability"`
+ }
+
+ type config struct {
+ Ignores []ignore `yaml:"ignore"`
+ }
+
+ tests := []struct {
+ name string
+ home string
+ cwd string
+ args []string
+ expected []ignore
+ err string
+ }{
+ {
+ name: "single explicit config",
+ home: configsDir,
+ cwd: cwd,
+ args: []string{
+ "-c",
+ path("dir1/.grype.yaml"),
+ },
+ expected: []ignore{
+ {
+ Vuln: "dir1-vuln1",
+ },
+ {
+ Vuln: "dir1-vuln2",
+ },
+ },
+ },
+ {
+ name: "multiple explicit config",
+ home: configsDir,
+ cwd: cwd,
+ args: []string{
+ "-c",
+ path("dir1/.grype.yaml"),
+ "-c",
+ path("dir2/.grype.yaml"),
+ },
+ expected: []ignore{
+ {
+ Vuln: "dir1-vuln1",
+ },
+ {
+ Vuln: "dir1-vuln2",
+ },
+ {
+ Vuln: "dir2-vuln1",
+ },
+ {
+ Vuln: "dir2-vuln2",
+ },
+ },
+ },
+ {
+ name: "empty profile override",
+ home: configsDir,
+ cwd: cwd,
+ args: []string{
+ "-c",
+ path("dir1/.grype.yaml"),
+ "-c",
+ path("dir2/.grype.yaml"),
+ "--profile",
+ "no-ignore",
+ },
+ expected: []ignore{},
+ },
+ {
+ name: "no profiles defined",
+ home: configsDir,
+ cwd: configsDir,
+ args: []string{
+ "-c",
+ path("dir3/.grype.yaml"),
+ "--profile",
+ "invalid",
+ },
+ err: "not found in any configuration files",
+ },
+ {
+ name: "invalid profile name",
+ home: configsDir,
+ cwd: cwd,
+ args: []string{
+ "-c",
+ path("dir1/.grype.yaml"),
+ "-c",
+ path("dir2/.grype.yaml"),
+ "--profile",
+ "alt",
+ },
+ err: "profile not found",
+ },
+ {
+ name: "explicit with profile override",
+ home: configsDir,
+ cwd: cwd,
+ args: []string{
+ "-c",
+ path("dir1/.grype.yaml"),
+ "-c",
+ path("dir2/.grype.yaml"),
+ "--profile",
+ "alt-ignore",
+ },
+ expected: []ignore{
+ {
+ Vuln: "dir1-alt-vuln1", // dir1 is still first
+ },
+ {
+ Vuln: "dir1-alt-vuln2", // dir1 is still first
+ },
+ {
+ Vuln: "dir2-alt-vuln1",
+ },
+ {
+ Vuln: "dir2-alt-vuln2",
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ require.NoError(t, os.Chdir(test.cwd))
+ defer func() { require.NoError(t, os.Chdir(cwd)) }()
+ env := map[string]string{
+ "HOME": test.home,
+ "XDG_CONFIG_HOME": test.home,
+ }
+ _, stdout, stderr := runGrype(t, env, append([]string{"config", "--load"}, test.args...)...)
+ if test.err != "" {
+ require.Contains(t, stderr, test.err)
+ return
+ } else {
+ require.Empty(t, stderr)
+ }
+ got := config{}
+ err = yaml.NewDecoder(strings.NewReader(stdout)).Decode(&got)
+ require.NoError(t, err)
+ require.Equal(t, test.expected, got.Ignores)
+ })
+ }
+}
diff --git a/test/cli/db_providers_test.go b/test/cli/db_providers_test.go
new file mode 100644
index 00000000000..69f642629a1
--- /dev/null
+++ b/test/cli/db_providers_test.go
@@ -0,0 +1,63 @@
+package cli
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestDBProviders(t *testing.T) {
+ tests := []struct {
+ name string
+ args []string
+ env map[string]string
+ assertions []traitAssertion
+ }{
+ {
+ name: "db providers command",
+ args: []string{"db", "providers"},
+ assertions: []traitAssertion{
+ assertNoStderr,
+ assertDbProvidersTableReport,
+ },
+ },
+ {
+ name: "db providers command help",
+ args: []string{"db", "providers", "-h"},
+ assertions: []traitAssertion{
+ assertInOutput("List vulnerability providers that are in the database"),
+ assertNoStderr,
+ },
+ },
+ {
+ name: "db providers command with table output flag",
+ args: []string{"db", "providers", "-o", "table"},
+ assertions: []traitAssertion{
+ assertNoStderr,
+ assertDbProvidersTableReport,
+ },
+ },
+ {
+ name: "db providers command with json output flag",
+ args: []string{"db", "providers", "-o", "json"},
+ assertions: []traitAssertion{
+ assertInOutput("processor"),
+ assertNoStderr,
+ assertJsonReport,
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ cmd, stdout, stderr := runGrype(t, test.env, test.args...)
+ for _, traitAssertionFn := range test.assertions {
+ traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
+ }
+ if t.Failed() {
+ t.Log("STDOUT:\n", stdout)
+ t.Log("STDERR:\n", stderr)
+ t.Log("COMMAND:", strings.Join(cmd.Args, " "))
+ }
+ })
+ }
+}
diff --git a/test/cli/db_validations_test.go b/test/cli/db_validations_test.go
index f1b77bff959..ff6adacbde0 100644
--- a/test/cli/db_validations_test.go
+++ b/test/cli/db_validations_test.go
@@ -1,35 +1,465 @@
package cli
import (
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
"strings"
"testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ v6 "github.com/anchore/grype/grype/db/v6"
+ dbtest "github.com/anchore/grype/grype/db/v6/testutil"
+ "github.com/anchore/grype/internal/schemaver"
)
func TestDBValidations(t *testing.T) {
+ invalidUpdateURL := fmt.Sprintf("https://localhost:%v", availablePort())
+ expiredDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(-24*24*time.Hour)).SetDBVersion(6, 0, 0).Start() // 24 days old
+ yesterdayDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(-24*time.Hour)).SetDBVersion(6, 0, 0).Start() // 24 hours old
+ todayDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now()).SetDBVersion(6, 0, 0).Start() // just built
+ todayDbNewerVersionURL := dbtest.NewServer(t).SetDBBuilt(time.Now()).SetDBVersion(6, 0, 1).Start() // just built
+ todayDbOlderVersionURL := dbtest.NewServer(t).SetDBBuilt(time.Now()).SetDBVersion(5, 9, 9).Start() // just built
+ notFoundDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(3 * time.Hour)).WithHandler(http.NotFound).Start()
+
+ // common setup functions
+ type setupFunc = func(t *testing.T, dir string)
+ setup := func(funcs ...setupFunc) setupFunc {
+ return func(t *testing.T, dir string) {
+ for _, f := range funcs {
+ f(t, dir)
+ }
+ }
+ }
+
+ setupDb := func(url string) setupFunc {
+ return func(t *testing.T, dir string) {
+ cmd, stdout, stderr := runGrype(t, map[string]string{
+ "GRYPE_DB_CACHE_DIR": dir,
+ "GRYPE_DB_UPDATE_URL": url,
+ }, "db", "update", "-vvv")
+ assertInOutput("downloading new vulnerability DB")(t, stdout, stderr, cmd.ProcessState.ExitCode())
+ assertSucceedingReturnCode(t, stdout, stderr, cmd.ProcessState.ExitCode())
+ }
+ }
+ setupExpiredDb := setupDb(expiredDbURL)
+ setupYesterdayDb := setupDb(yesterdayDbURL)
+ setupTodayDb := setupDb(todayDbURL)
+
+ dbFilePath := func(dir string) string {
+ return filepath.Join(dir, strconv.Itoa(v6.ModelVersion), "vulnerability.db")
+ }
+
+ corruptDb := func(t *testing.T, dir string) {
+ err := os.Truncate(dbFilePath(dir), 20)
+ require.NoError(t, err)
+ }
+
+ moveDbToBackup := func(t *testing.T, dir string) {
+ err := os.Rename(dbFilePath(dir), filepath.Join(dir, "db.old"))
+ require.NoError(t, err)
+ }
+
+ restoreDbFromBackup := func(t *testing.T, dir string) {
+ // replace with valid db, which doesn't match the hash
+ err := os.Rename(filepath.Join(dir, "db.old"), dbFilePath(dir))
+ require.NoError(t, err)
+ }
+
+ deleteDb := func(t *testing.T, dir string) {
+ err := os.Remove(dbFilePath(dir))
+ require.NoError(t, err)
+ }
+
+ // common asserts
+ assertDbDownloaded := assertInOutput("downloading new vulnerability DB")
+ assertDbNotDownloaded := assertNotInOutput("downloading new vulnerability DB")
+ assertScanRan := assertInOutput("No vulnerabilities found")
+ assertDbLoadFailed := assertInOutput("failed to load vulnerability db")
+ assertDbLoadNotAtempted := assertNotInOutput("failed to load vulnerability db")
+ assertDbNotFound := assertInOutput("No installed DB version found")
+ assertCheckedForDbUpdate := assertInOutput("checking for available database updates")
+ assertDbHashed := assertInOutput("captured DB digest")
+ assertUpdateMessageDisplayed := assertInOutput("update to the latest db")
+ cmdAliases := map[string]string{"scan": "pkg:no/thing@0"} // scan: matching a purl with no vulnerabilities
+
+ // ensure we have grype built and ready
+ runGrype(t, map[string]string{}, "config")
+
tests := []struct {
- name string
- args []string
- env map[string]string
- assertions []traitAssertion
+ name string // the portion of the name before `:` is the command to run from cmdAliases above or the literal value
+ setup setupFunc // setup to run before test cmd
+ dbUpdateURL string // update url to use, e.g. todayDbURL
+ dbRequireUpdate bool // whether an update check is required
+ dbMaxUpdateCheckFrequency string // max update check frequency, defaults to 0 to always check
+ dbValidateHash bool // whether to validate existing db by hash
+ dbValidateAge bool // whether to validate existing db age
+ dbCaCert string // ca cert file, if set
+ assertions []traitAssertion
}{
{
- // regression: do not panic on bad DB load
- name: "fail on bad DB load",
- args: []string{"-vv", "dir:."},
- env: map[string]string{
- "GRYPE_DB_CACHE_DIR": t.TempDir(),
- "GRYPE_DB_CA_CERT": "./does-not-exist.crt",
+ name: "scan: new install downloads successfully",
+ setup: nil,
+ dbUpdateURL: yesterdayDbURL,
+ assertions: []traitAssertion{
+ assertDbDownloaded,
+ assertScanRan,
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "scan: existing db updates successfully",
+ setup: setupYesterdayDb,
+ dbUpdateURL: todayDbURL,
+ assertions: []traitAssertion{
+ assertDbHashed,
+ assertDbDownloaded,
+ assertScanRan,
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "scan: existing db skips update when same",
+ setup: setupYesterdayDb,
+ dbUpdateURL: yesterdayDbURL,
+ assertions: []traitAssertion{
+ assertDbNotDownloaded,
+ assertScanRan,
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "scan: existing db skips update when newer",
+ setup: setupTodayDb,
+ dbUpdateURL: yesterdayDbURL,
+ assertions: []traitAssertion{
+ assertDbNotDownloaded,
+ assertScanRan,
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "scan: continues on corrupt db no update",
+ setup: setup(setupYesterdayDb, corruptDb),
+ dbUpdateURL: yesterdayDbURL,
+ assertions: []traitAssertion{
+ assertDbDownloaded,
+ assertScanRan,
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "db check: continues on corrupt db no update",
+ setup: setup(setupYesterdayDb, corruptDb),
+ dbUpdateURL: yesterdayDbURL,
+ assertions: []traitAssertion{
+ assertDbNotFound,
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "db check: continues on corrupt db with update",
+ setup: setup(setupYesterdayDb, corruptDb),
+ dbUpdateURL: todayDbURL,
+ assertions: []traitAssertion{
+ assertDbNotFound,
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "db status: fails with corrupt db no update",
+ setup: setup(setupYesterdayDb, corruptDb),
+ dbUpdateURL: yesterdayDbURL,
+ assertions: []traitAssertion{
+ assertDbNotDownloaded,
+ assertInOutput("failed to read DB description"),
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "db status: fails with corrupt db with update",
+ setup: setup(setupYesterdayDb, corruptDb),
+ dbUpdateURL: todayDbURL,
+ assertions: []traitAssertion{
+ assertDbNotDownloaded,
+ assertInOutput("failed to read DB description"),
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "scan: missing db downloads a new one",
+ setup: setup(setupYesterdayDb, deleteDb),
+ dbUpdateURL: todayDbURL,
+ assertions: []traitAssertion{
+ assertDbDownloaded,
+ assertScanRan,
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "db check: missing db does not affect no update",
+ setup: setup(setupYesterdayDb, deleteDb),
+ dbUpdateURL: yesterdayDbURL,
+ assertions: []traitAssertion{
+ assertDbNotFound,
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "db check: missing db does not affect with update",
+ setup: setup(setupYesterdayDb, deleteDb),
+ dbUpdateURL: todayDbURL,
+ assertions: []traitAssertion{
+ assertDbNotFound,
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "db status: missing db returns error",
+ setup: setup(setupYesterdayDb, deleteDb),
+ dbUpdateURL: todayDbURL,
+ assertions: []traitAssertion{
+ assertInOutput("database does not exist"),
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "db status: valid db fails with hash mismatch",
+ setup: setup(setupYesterdayDb, moveDbToBackup, setupTodayDb, deleteDb, restoreDbFromBackup),
+ dbUpdateURL: invalidUpdateURL,
+ dbValidateHash: true,
+ assertions: []traitAssertion{
+ assertInOutput("bad db checksum"),
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "db check: valid db with hash mismatch",
+ setup: setup(setupYesterdayDb, moveDbToBackup, setupTodayDb, deleteDb, restoreDbFromBackup),
+ dbUpdateURL: invalidUpdateURL,
+ dbValidateHash: true,
+ assertions: []traitAssertion{
+ assertDbLoadNotAtempted,
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "scan: valid db fails with hash mismatch",
+ setup: setup(setupYesterdayDb, moveDbToBackup, setupTodayDb, deleteDb, restoreDbFromBackup),
+ dbUpdateURL: invalidUpdateURL,
+ dbValidateHash: true,
+ assertions: []traitAssertion{
+ assertInOutput("bad db checksum"),
+ assertDbLoadFailed,
+ assertDbNotDownloaded,
+ // notification mentions grype db delete and grype db update
+ assertInOutput("grype db delete"),
+ assertInOutput("grype db update"),
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "scan: missing import.json",
+ setup: setup(setupYesterdayDb, func(t *testing.T, dir string) {
+ require.NoError(t, os.Remove(filepath.Join(filepath.Dir(dbFilePath(dir)), "import.json")))
+ }),
+ dbUpdateURL: invalidUpdateURL,
+ dbValidateHash: true,
+ assertions: []traitAssertion{
+ assertInOutput("no import metadata"),
+ assertDbLoadFailed,
+ assertDbNotDownloaded,
+ // notification mentions grype db delete and grype db update
+ assertInOutput("grype db delete"),
+ assertInOutput("grype db update"),
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "scan: update check error with valid db continues",
+ setup: setupYesterdayDb,
+ dbUpdateURL: notFoundDbURL,
+ dbRequireUpdate: false,
+ assertions: []traitAssertion{
+ assertInOutput("error updating db"),
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "scan: update check error with valid db fails when require update",
+ setup: setupYesterdayDb,
+ dbUpdateURL: notFoundDbURL,
+ dbRequireUpdate: true,
+ assertions: []traitAssertion{
+ assertInOutput("unable to update db"),
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "db check: update check error with valid db fails",
+ setup: setupYesterdayDb,
+ dbUpdateURL: notFoundDbURL,
+ dbRequireUpdate: false,
+ assertions: []traitAssertion{
+ assertInOutput("unable to check for vulnerability database update"),
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "scan: database older than max age fails when unable to update",
+ setup: setupExpiredDb,
+ dbUpdateURL: notFoundDbURL,
+ dbValidateAge: true,
+ assertions: []traitAssertion{
+ assertInOutput("the vulnerability database was built"),
+ assertFailingReturnCode,
+ },
+ },
+ {
+ name: "scan: database older than max age succeeds with update",
+ setup: setupExpiredDb,
+ dbUpdateURL: todayDbURL,
+ dbValidateAge: true,
+ assertions: []traitAssertion{
+ assertDbDownloaded,
+ assertScanRan,
+ assertSucceedingReturnCode,
},
+ },
+ {
+ name: "scan: no panic on bad cert configuration",
+ dbCaCert: "./does-not-exist.crt",
assertions: []traitAssertion{
assertInOutput("failed to load vulnerability db"),
assertFailingReturnCode,
},
},
+ {
+ name: "db check: always check for updates regardless of frequency",
+ setup: setupYesterdayDb,
+ dbUpdateURL: todayDbURL,
+ dbMaxUpdateCheckFrequency: "10h",
+ assertions: []traitAssertion{
+ assertCheckedForDbUpdate,
+ assertUpdateMessageDisplayed,
+ func(tb testing.TB, stdout, stderr string, rc int) {
+ require.Equal(t, 100, rc)
+ },
+ },
+ },
+ {
+ name: "db update: always update regardless of frequency",
+ setup: setupYesterdayDb,
+ dbUpdateURL: todayDbURL,
+ dbMaxUpdateCheckFrequency: "10h",
+ assertions: []traitAssertion{
+ assertCheckedForDbUpdate,
+ assertDbDownloaded,
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "scan: ensure db update frequency config is respected",
+ setup: setupYesterdayDb,
+ dbUpdateURL: todayDbURL,
+ dbMaxUpdateCheckFrequency: "10h", // last check was during setup, much more recently than 10h ago
+ assertions: []traitAssertion{
+ assertNotInOutput("no max-frequency set for update check"),
+ assertNotInOutput("checking for available database updates"),
+ assertDbNotDownloaded,
+ assertInOutput("max-update-check-frequency: 10h"),
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "scan: ensure newer db version with older grype is not hydrated",
+ setup: setup(setupDb(todayDbNewerVersionURL), func(t *testing.T, dir string) {
+ // change the hydration version to a newer version that this grype
+ metaFile := filepath.Join(filepath.Dir(dbFilePath(dir)), v6.ImportMetadataFileName)
+ contents, err := os.ReadFile(metaFile)
+ require.NoError(t, err)
+ meta := v6.ImportMetadata{}
+ err = json.Unmarshal(contents, &meta)
+ require.NoError(t, err)
+ meta.ClientVersion = schemaver.New(v6.ModelVersion, v6.Revision, v6.Addition+1).String()
+ contents, err = json.Marshal(meta)
+ require.NoError(t, err)
+ err = os.WriteFile(metaFile, contents, 0x777)
+ require.NoError(t, err)
+ }),
+ dbUpdateURL: todayDbNewerVersionURL,
+ assertions: []traitAssertion{
+ assertInOutput("DB rehydration not needed"),
+ assertDbNotDownloaded,
+ assertSucceedingReturnCode,
+ },
+ },
+ {
+ name: "scan: ensure older db version with newer db version hydrated",
+ setup: setup(setupDb(todayDbNewerVersionURL), func(t *testing.T, dir string) {
+ // change the hydration version to a older version that this grype
+ metaFile := filepath.Join(filepath.Dir(dbFilePath(dir)), v6.ImportMetadataFileName)
+ contents, err := os.ReadFile(metaFile)
+ require.NoError(t, err)
+ meta := v6.ImportMetadata{}
+ err = json.Unmarshal(contents, &meta)
+ require.NoError(t, err)
+ meta.ClientVersion = schemaver.New(v6.ModelVersion-1, v6.Revision, v6.Addition).String()
+ contents, err = json.Marshal(meta)
+ require.NoError(t, err)
+ err = os.WriteFile(metaFile, contents, 0x777)
+ require.NoError(t, err)
+ }),
+ dbUpdateURL: todayDbOlderVersionURL,
+ assertions: []traitAssertion{
+ assertInOutput("rehydrating DB"),
+ assertDbNotDownloaded,
+ assertSucceedingReturnCode,
+ },
+ },
}
for _, test := range tests {
+ test := test
t.Run(test.name, func(t *testing.T) {
- cmd, stdout, stderr := runGrype(t, test.env, test.args...)
+ t.Parallel()
+
+ dbDir := t.TempDir()
+ if test.setup != nil {
+ test.setup(t, dbDir)
+ }
+
+ // set up values
+ env := map[string]string{
+ "GRYPE_DB_CACHE_DIR": dbDir,
+ "GRYPE_DB_UPDATE_URL": defaultValue(test.dbUpdateURL, invalidUpdateURL),
+ "GRYPE_DB_VALIDATE_BY_HASH_ON_START": fmt.Sprintf("%v", defaultValue(test.dbValidateHash, false)),
+ "GRYPE_DB_VALIDATE_AGE": fmt.Sprintf("%v", defaultValue(test.dbValidateAge, false)),
+ "GRYPE_DB_MAX_UPDATE_CHECK_FREQUENCY": defaultValue(test.dbMaxUpdateCheckFrequency, "0"),
+ }
+ if test.dbValidateAge {
+ env["GRYPE_DB_MAX_ALLOWED_BUILT_AGE"] = "48h" // expired db is 24 days old
+ }
+ if test.dbCaCert != "" {
+ env["GRYPE_DB_CA_CERT"] = test.dbCaCert
+ }
+ if test.dbRequireUpdate {
+ env["GRYPE_DB_REQUIRE_UPDATE_CHECK"] = "true"
+ }
+
+ // test name before : is command args
+ args := strings.Split(test.name, ":")
+ args = strings.Split(args[0], " ")
+ if cmd := cmdAliases[args[0]]; cmd != "" {
+ args[0] = cmd
+ }
+ cmd, stdout, stderr := runGrype(t, env, append(args, "-vvv")...)
for _, traitAssertionFn := range test.assertions {
traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
@@ -41,3 +471,22 @@ func TestDBValidations(t *testing.T) {
})
}
}
+
+func defaultValue[T comparable](value T, defaultValue T) T {
+ var empty T
+ if value == empty {
+ return defaultValue
+ }
+ return value
+}
+
+func availablePort() int {
+ if a, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0"); err == nil {
+ var l *net.TCPListener
+ if l, err = net.ListenTCP("tcp", a); err == nil {
+ defer func() { _ = l.Close() }()
+ return l.Addr().(*net.TCPAddr).Port
+ }
+ }
+ panic("unable to get port")
+}
diff --git a/test/cli/registry_auth_test.go b/test/cli/registry_auth_test.go
index a06c28a041c..0ac8ef92af4 100644
--- a/test/cli/registry_auth_test.go
+++ b/test/cli/registry_auth_test.go
@@ -16,9 +16,9 @@ func TestRegistryAuth(t *testing.T) {
name: "fallback to keychain",
args: []string{"-vv", "registry:localhost:5000/something:latest"},
assertions: []traitAssertion{
- assertInOutput("source=OciRegistry"),
+ assertInOutput("from registry"),
assertInOutput("localhost:5000/something:latest"),
- assertInOutput("no registry credentials configured, using the default keychain"),
+ assertInOutput(`no registry credentials configured for "localhost:5000", using the default keychain`),
},
},
{
@@ -30,7 +30,7 @@ func TestRegistryAuth(t *testing.T) {
"GRYPE_REGISTRY_AUTH_PASSWORD": "password",
},
assertions: []traitAssertion{
- assertInOutput("source=OciRegistry"),
+ assertInOutput("from registry"),
assertInOutput("localhost:5000/something:latest"),
assertInOutput(`using basic auth for registry "localhost:5000"`),
},
@@ -40,10 +40,10 @@ func TestRegistryAuth(t *testing.T) {
args: []string{"-vv", "registry:localhost:5000/something:latest"},
env: map[string]string{
"GRYPE_REGISTRY_AUTH_AUTHORITY": "localhost:5000",
- "GRYPE_REGISTRY_AUTH_TOKEN": "token",
+ "GRYPE_REGISTRY_AUTH_TOKEN": "my-token",
},
assertions: []traitAssertion{
- assertInOutput("source=OciRegistry"),
+ assertInOutput("from registry"),
assertInOutput("localhost:5000/something:latest"),
assertInOutput(`using token for registry "localhost:5000"`),
},
@@ -55,9 +55,9 @@ func TestRegistryAuth(t *testing.T) {
"GRYPE_REGISTRY_AUTH_AUTHORITY": "localhost:5000",
},
assertions: []traitAssertion{
- assertInOutput("source=OciRegistry"),
+ assertInOutput("from registry"),
assertInOutput("localhost:5000/something:latest"),
- assertInOutput(`no registry credentials configured, using the default keychain`),
+ assertInOutput(`no registry credentials configured for "localhost:5000", using the default keychain`),
},
},
{
@@ -70,6 +70,17 @@ func TestRegistryAuth(t *testing.T) {
assertInOutput("insecure-use-http: true"),
},
},
+ {
+ name: "use tls configuration",
+ args: []string{"-vvv", "registry:localhost:5000/something:latest"},
+ env: map[string]string{
+ "GRYPE_REGISTRY_AUTH_TLS_CERT": "place.crt",
+ "GRYPE_REGISTRY_AUTH_TLS_KEY": "place.key",
+ },
+ assertions: []traitAssertion{
+ assertInOutput("using custom TLS credentials from"),
+ },
+ },
}
for _, test := range tests {
diff --git a/test/cli/sbom_input_test.go b/test/cli/sbom_input_test.go
index b6ae2149611..428c7e0922e 100644
--- a/test/cli/sbom_input_test.go
+++ b/test/cli/sbom_input_test.go
@@ -70,7 +70,7 @@ func TestSBOMInput_FromStdin(t *testing.T) {
input: "./test-fixtures/empty.json",
args: []string{"-c", "../grype-test-config.yaml"},
wantErr: require.Error,
- wantOutput: "unable to decode sbom: unable to identify format",
+ wantOutput: "unable to decode sbom: sbom format not recognized",
},
{
name: "sbom",
diff --git a/test/cli/test-fixtures/configs/dir1/.grype.yaml b/test/cli/test-fixtures/configs/dir1/.grype.yaml
new file mode 100644
index 00000000000..6caefc213bc
--- /dev/null
+++ b/test/cli/test-fixtures/configs/dir1/.grype.yaml
@@ -0,0 +1,12 @@
+ignore:
+ - vulnerability: 'dir1-vuln1'
+ - vulnerability: 'dir1-vuln2'
+
+profiles:
+ no-ignore:
+ ignore: []
+
+ alt-ignore:
+ ignore:
+ - vulnerability: 'dir1-alt-vuln1'
+ - vulnerability: 'dir1-alt-vuln2'
diff --git a/test/cli/test-fixtures/configs/dir2/.grype.yaml b/test/cli/test-fixtures/configs/dir2/.grype.yaml
new file mode 100644
index 00000000000..8e1ae8bab74
--- /dev/null
+++ b/test/cli/test-fixtures/configs/dir2/.grype.yaml
@@ -0,0 +1,9 @@
+ignore:
+ - vulnerability: 'dir2-vuln1'
+ - vulnerability: 'dir2-vuln2'
+
+profiles:
+ alt-ignore:
+ ignore:
+ - vulnerability: 'dir2-alt-vuln1'
+ - vulnerability: 'dir2-alt-vuln2'
diff --git a/test/cli/test-fixtures/configs/dir3/.grype.yaml b/test/cli/test-fixtures/configs/dir3/.grype.yaml
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/test/cli/test-fixtures/image-java-subprocess/Dockerfile b/test/cli/test-fixtures/image-java-subprocess/Dockerfile
index f623bee06b8..40ffb95ec30 100644
--- a/test/cli/test-fixtures/image-java-subprocess/Dockerfile
+++ b/test/cli/test-fixtures/image-java-subprocess/Dockerfile
@@ -1,4 +1,4 @@
-FROM openjdk:15-slim-buster
+FROM openjdk:15-slim-buster@sha256:1e069bf1c5c23adde58b29b82281b862e473d698ce7cc4e164194a0a2a1c044a
COPY app.java /
ENV PATH="/app/bin:${PATH}"
-WORKDIR /
\ No newline at end of file
+WORKDIR /
diff --git a/test/cli/test-fixtures/image-node-subprocess/Dockerfile b/test/cli/test-fixtures/image-node-subprocess/Dockerfile
index f79e5513962..5e09a3e24f4 100644
--- a/test/cli/test-fixtures/image-node-subprocess/Dockerfile
+++ b/test/cli/test-fixtures/image-node-subprocess/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:16-stretch
+FROM node:16-stretch@sha256:5810de52349af302a2c5dddf0a3f31174ef65d0eed8985959a5e83bb1084b79b
COPY app.js /
ENV PATH="/app/bin:${PATH}"
-WORKDIR /
\ No newline at end of file
+WORKDIR /
diff --git a/test/cli/test-fixtures/test-ignore-reason/config-with-ignore.yaml b/test/cli/test-fixtures/test-ignore-reason/config-with-ignore.yaml
new file mode 100644
index 00000000000..73c063375d9
--- /dev/null
+++ b/test/cli/test-fixtures/test-ignore-reason/config-with-ignore.yaml
@@ -0,0 +1,4 @@
+check-for-app-update: false
+ignore:
+ - vulnerability: CVE-2021-42385
+ reason: test reason for vulnerability being ignored
diff --git a/test/cli/test-fixtures/test-ignore-reason/sbom.json b/test/cli/test-fixtures/test-ignore-reason/sbom.json
new file mode 100644
index 00000000000..efb1879667f
--- /dev/null
+++ b/test/cli/test-fixtures/test-ignore-reason/sbom.json
@@ -0,0 +1,188 @@
+{
+ "artifacts": [
+ {
+ "id": "20a169629a73fdbd",
+ "name": "busybox",
+ "version": "1.31.1",
+ "type": "binary",
+ "foundBy": "binary-cataloger",
+ "locations": [
+ {
+ "path": "/bin/[",
+ "layerID": "sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f",
+ "annotations": {
+ "evidence": "primary"
+ }
+ }
+ ],
+ "licenses": [],
+ "language": "",
+ "cpes": [
+ "cpe:2.3:a:busybox:busybox:1.31.1:*:*:*:*:*:*:*",
+ "cpe:2.3:a:busybox:busybox:1.31.1:*:*:*:*:*:*:*"
+ ],
+ "purl": "",
+ "metadataType": "BinaryMetadata",
+ "metadata": {
+ "matches": [
+ {
+ "classifier": "busybox-binary",
+ "location": {
+ "path": "/bin/[",
+ "layerID": "sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f",
+ "annotations": {
+ "evidence": "primary"
+ }
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "artifactRelationships": [
+ {
+ "parent": "1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853",
+ "child": "20a169629a73fdbd",
+ "type": "contains"
+ },
+ {
+ "parent": "20a169629a73fdbd",
+ "child": "1c3ded193f8808da",
+ "type": "evident-by"
+ }
+ ],
+ "files": [
+ {
+ "id": "1c3ded193f8808da",
+ "location": {
+ "path": "/bin/[",
+ "layerID": "sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f"
+ }
+ }
+ ],
+ "source": {
+ "id": "1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853",
+ "name": "busybox",
+ "version": "1.31",
+ "type": "image",
+ "metadata": {
+ "userInput": "busybox:1.31",
+ "imageID": "sha256:19d689bc58fd64da6a46d46512ea965a12b6bfb5b030400e21bc0a04c4ff155e",
+ "manifestDigest": "sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853",
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "tags": [],
+ "imageSize": 1384134,
+ "layers": [
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "digest": "sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f",
+ "size": 1384134
+ }
+ ],
+ "manifest": "ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ5NCwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6MTlkNjg5YmM1OGZkNjRkYTZhNDZkNDY1MTJlYTk2NWExMmI2YmZiNWIwMzA0MDBlMjFiYzBhMDRjNGZmMTU1ZSIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDgxNTQwMCwKICAgICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6ZmQ0NDAxNmUzZDNlZGI4ZDFkOGIxYTMzNTdmNzcwOGE4Mzg4ZTQ2MjI1MDBkMzZmZGUzYThjZGZiMjNmYmFjOCIKICAgICAgfQogICBdCn0=",
+ "config": "eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbInNoIl0sIkFyZ3NFc2NhcGVkIjp0cnVlLCJJbWFnZSI6InNoYTI1NjpmNDFmZmIxYWI1MWNhMDg3YzdkN2E3MWM5NDUzMGNmOWM4MjFkZDM3Zjc2ZGU0NjY2NTZmNjM1NDE0NGQzYmQyIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6bnVsbH0sImNvbnRhaW5lciI6IjBlY2U0NzFlM2MyMGFmYTQyYWM2ZTRhNzBlYTc0MDFmM2ViNGNiNzUzZmQ0MzYyZDA3ZDNmNWI1ZjFlODhhZDEiLCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiMGVjZTQ3MWUzYzIwIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiJdLCJDbWQiOlsiL2Jpbi9zaCIsIi1jIiwiIyhub3ApICIsIkNNRCBbXCJzaFwiXSJdLCJBcmdzRXNjYXBlZCI6dHJ1ZSwiSW1hZ2UiOiJzaGEyNTY6ZjQxZmZiMWFiNTFjYTA4N2M3ZDdhNzFjOTQ1MzBjZjljODIxZGQzN2Y3NmRlNDY2NjU2ZjYzNTQxNDRkM2JkMiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOnt9fSwiY3JlYXRlZCI6IjIwMjAtMDYtMDJUMjE6Mzk6NDUuMzc0Mjg5MDQxWiIsImRvY2tlcl92ZXJzaW9uIjoiMTguMDkuNyIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIwLTA2LTAyVDIxOjM5OjQ0LjUzMDIwOTg5MVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6MDdkOTQ2NmVkMWExMDkxNmY0ODIzZGI2OWY0YzU4NDg0Y2U3MDIyMWMzYzk0YTBmMmE4MDRmNTM3MWE2Mjc2NSBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjAtMDYtMDJUMjE6Mzk6NDUuMzc0Mjg5MDQxWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcInNoXCJdIiwiZW1wdHlfbGF5ZXIiOnRydWV9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OjdjZTM3ODQ0Y2E3NTYwMGRiY2JlMDg1ODU4ODQ1YzViOTJiNjEwOWRiM2M4YzFhZTZlYjg4N2FhYjkxYWQwNGYiXX19",
+ "repoDigests": [
+ "index.docker.io/library/busybox@sha256:95cf004f559831017cdf4628aaf1bb30133677be8702a8c5f2994629f637a209"
+ ],
+ "architecture": "arm64",
+ "os": "linux"
+ }
+ },
+ "distro": {
+ "prettyName": "BusyBox v1.31.1",
+ "name": "busybox",
+ "id": "busybox",
+ "idLike": [
+ "busybox"
+ ],
+ "version": "1.31.1",
+ "versionID": "1.31.1"
+ },
+ "descriptor": {
+ "name": "syft",
+ "version": "0.94.0",
+ "configuration": {
+ "catalogers": null,
+ "package": {
+ "cataloger": {
+ "enabled": true,
+ "scope": "Squashed"
+ },
+ "search-unindexed-archives": false,
+ "search-indexed-archives": true
+ },
+ "golang": {
+ "search-local-mod-cache-licenses": false,
+ "local-mod-cache-dir": "",
+ "search-remote-licenses": false,
+ "proxy": "",
+ "no-proxy": ""
+ },
+ "linux-kernel": {
+ "catalog-modules": true
+ },
+ "python": {
+ "guess-unpinned-requirements": false
+ },
+ "file-metadata": {
+ "cataloger": {
+ "enabled": false,
+ "scope": "Squashed"
+ },
+ "digests": [
+ "sha256"
+ ]
+ },
+ "file-classification": {
+ "cataloger": {
+ "enabled": false,
+ "scope": "Squashed"
+ }
+ },
+ "file-contents": {
+ "cataloger": {
+ "enabled": false,
+ "scope": "Squashed"
+ },
+ "skip-files-above-size": 1048576,
+ "globs": null
+ },
+ "secrets": {
+ "cataloger": {
+ "enabled": false,
+ "scope": "AllLayers"
+ },
+ "additional-patterns": null,
+ "exclude-pattern-names": null,
+ "reveal-values": false,
+ "skip-files-above-size": 1048576
+ },
+ "registry": {
+ "insecure-skip-tls-verify": false,
+ "insecure-use-http": false,
+ "auth": null,
+ "ca-cert": ""
+ },
+ "exclude": [],
+ "platform": "",
+ "name": "",
+ "source": {
+ "name": "",
+ "version": "",
+ "file": {
+ "digests": [
+ "sha256"
+ ]
+ }
+ },
+ "parallelism": 1,
+ "default-image-pull-source": "",
+ "base-path": "",
+ "exclude-binary-overlap-by-ownership": true
+ }
+ },
+ "schema": {
+ "version": "11.0.1",
+ "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-11.0.1.json"
+ }
+}
diff --git a/test/cli/test-fixtures/test-ignore-reason/template-with-ignore-reasons b/test/cli/test-fixtures/test-ignore-reason/template-with-ignore-reasons
new file mode 100644
index 00000000000..e4f5289a2b6
--- /dev/null
+++ b/test/cli/test-fixtures/test-ignore-reason/template-with-ignore-reasons
@@ -0,0 +1,4 @@
+The following vulnerabilities are considered irrelevant:
+{{- range .IgnoredMatches}}
+ {{.Vulnerability.ID}} ({{ range $air := .AppliedIgnoreRules }}{{ $air.Reason }}{{ end }})
+{{- end}}
diff --git a/test/cli/trait_assertions_test.go b/test/cli/trait_assertions_test.go
index fa89d35ea08..59aad39bcad 100644
--- a/test/cli/trait_assertions_test.go
+++ b/test/cli/trait_assertions_test.go
@@ -1,6 +1,7 @@
package cli
import (
+ "encoding/json"
"strings"
"testing"
@@ -9,6 +10,13 @@ import (
type traitAssertion func(tb testing.TB, stdout, stderr string, rc int)
+func assertNoStderr(tb testing.TB, _, stderr string, _ int) {
+ tb.Helper()
+ if len(stderr) > 0 {
+ tb.Errorf("expected stderr to be empty, but wasn't")
+ }
+}
+
func assertInOutput(data string) traitAssertion {
return func(tb testing.TB, stdout, stderr string, _ int) {
tb.Helper()
@@ -32,3 +40,50 @@ func assertSucceedingReturnCode(tb testing.TB, _, _ string, rc int) {
tb.Errorf("expected to succeed but got rc=%d", rc)
}
}
+
+func assertRowInStdOut(row []string) traitAssertion {
+ return func(tb testing.TB, stdout, stderr string, _ int) {
+ tb.Helper()
+
+ for _, line := range strings.Split(stdout, "\n") {
+ lineMatched := false
+ for _, column := range row {
+ if !strings.Contains(line, column) {
+ // it wasn't this line
+ lineMatched = false
+ break
+ }
+ lineMatched = true
+ }
+ if lineMatched {
+ return
+ }
+ }
+ // none of the lines matched
+ tb.Errorf("expected stdout to contain %s, but it did not", strings.Join(row, " "))
+ }
+}
+
+func assertNotInOutput(notWanted string) traitAssertion {
+ return func(tb testing.TB, stdout, stderr string, _ int) {
+ if strings.Contains(stdout, notWanted) {
+ tb.Errorf("got unwanted %s in stdout %s", notWanted, stdout)
+ }
+ }
+}
+
+func assertJsonReport(tb testing.TB, stdout, _ string, _ int) {
+ tb.Helper()
+ var data interface{}
+
+ if err := json.Unmarshal([]byte(stdout), &data); err != nil {
+ tb.Errorf("expected to find a JSON report, but was unmarshalable: %+v", err)
+ }
+}
+
+func assertDbProvidersTableReport(tb testing.TB, stdout, _ string, _ int) {
+ tb.Helper()
+ if !strings.Contains(stdout, "NAME") || !strings.Contains(stdout, "DATE CAPTURED") {
+ tb.Errorf("expected to find a table report, but did not")
+ }
+}
diff --git a/test/cli/utils_test.go b/test/cli/utils_test.go
index ff14cb2ed5b..2de19bea693 100644
--- a/test/cli/utils_test.go
+++ b/test/cli/utils_test.go
@@ -6,7 +6,6 @@ import (
"io"
"os"
"os/exec"
- "path"
"path/filepath"
"runtime"
"strings"
@@ -27,38 +26,77 @@ func getFixtureImage(tb testing.TB, fixtureImageName string) string {
func getGrypeCommand(tb testing.TB, args ...string) *exec.Cmd {
tb.Helper()
+ argsWithConfig := args
+ if !grypeCommandHasConfigArg(argsWithConfig...) {
+ argsWithConfig = append(
+ []string{"-c", "../grype-test-config.yaml"},
+ args...,
+ )
+ }
return exec.Command(
getGrypeSnapshotLocation(tb, runtime.GOOS),
- append(
- []string{"-c", "../grype-test-config.yaml"},
- args...,
- )...,
+ argsWithConfig...,
)
}
-func getGrypeSnapshotLocation(tb testing.TB, goOS string) string {
- if os.Getenv("GRYPE_BINARY_LOCATION") != "" {
- // GRYPE_BINARY_LOCATION is the absolute path to the snapshot binary
- return os.Getenv("GRYPE_BINARY_LOCATION")
+func grypeCommandHasConfigArg(args ...string) bool {
+ for _, arg := range args {
+ if arg == "-c" || arg == "--config" {
+ return true
+ }
+ }
+ return false
+}
+
+func getGrypeSnapshotLocation(t testing.TB, goOS string) string {
+ // GRYPE_BINARY_LOCATION is the absolute path to the snapshot binary
+ const envKey = "GRYPE_BINARY_LOCATION"
+ if os.Getenv(envKey) != "" {
+ return os.Getenv(envKey)
}
+ loc := getGrypeBinaryLocationByOS(t, goOS)
+ buildBinary(t, loc)
+ _ = os.Setenv(envKey, loc)
+ return loc
+}
+func getGrypeBinaryLocationByOS(t testing.TB, goOS string) string {
// note: for amd64 we need to update the snapshot location with the v1 suffix
// see : https://goreleaser.com/customization/build/#why-is-there-a-_v1-suffix-on-amd64-builds
archPath := runtime.GOARCH
if runtime.GOARCH == "amd64" {
archPath = fmt.Sprintf("%s_v1", archPath)
}
-
+ executable := "grype"
+ // note: there is a subtle - vs _ difference between these versions
switch goOS {
+ case "windows":
+ executable += ".exe"
+ fallthrough
case "darwin", "linux":
- return path.Join(repoRoot(tb), fmt.Sprintf("snapshot/%s-build_%s_%s/grype", goOS, goOS, archPath))
+ return filepath.Join(repoRoot(t), "snapshot", fmt.Sprintf("%s-build_%s_%s", goOS, goOS, archPath), executable)
default:
- tb.Fatalf("unsupported OS: %s", runtime.GOOS)
+ t.Fatalf("unsupported OS: %s", runtime.GOOS)
}
return ""
}
+func buildBinary(t testing.TB, loc string) {
+ wd, err := os.Getwd()
+ require.NoError(t, err)
+ require.NoError(t, os.Chdir(repoRoot(t)))
+ defer func() {
+ require.NoError(t, os.Chdir(wd))
+ }()
+ t.Log("Building grype...")
+ c := exec.Command("go", "build", "-o", loc, "./cmd/grype")
+ c.Stdout = os.Stdout
+ c.Stderr = os.Stderr
+ c.Stdin = os.Stdin
+ require.NoError(t, c.Run())
+}
+
func getDockerRunCommand(tb testing.TB, args ...string) *exec.Cmd {
tb.Helper()
diff --git a/test/cli/version_cmd_test.go b/test/cli/version_cmd_test.go
new file mode 100644
index 00000000000..c043c8e08a0
--- /dev/null
+++ b/test/cli/version_cmd_test.go
@@ -0,0 +1,30 @@
+package cli
+
+import (
+ "testing"
+)
+
+func TestVersionCmdPrintsToStdout(t *testing.T) {
+ tests := []struct {
+ name string
+ env map[string]string
+ assertions []traitAssertion
+ }{
+ {
+ name: "version command prints to stdout",
+ assertions: []traitAssertion{
+ assertInOutput("Version:"),
+ assertNoStderr,
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ pkgCmd, pkgsStdout, pkgsStderr := runGrype(t, test.env, "version")
+ for _, traitFn := range test.assertions {
+ traitFn(t, pkgsStdout, pkgsStderr, pkgCmd.ProcessState.ExitCode())
+ }
+ })
+ }
+}
diff --git a/test/grype-test-config.yaml b/test/grype-test-config.yaml
index 4b4d63bf0f0..21b7d303846 100644
--- a/test/grype-test-config.yaml
+++ b/test/grype-test-config.yaml
@@ -1 +1,2 @@
check-for-app-update: false
+
diff --git a/test/install/0_search_for_asset_test.sh b/test/install/0_checksums_test.sh
similarity index 54%
rename from test/install/0_search_for_asset_test.sh
rename to test/install/0_checksums_test.sh
index 939678289d7..3678a2a26ec 100755
--- a/test/install/0_search_for_asset_test.sh
+++ b/test/install/0_checksums_test.sh
@@ -38,3 +38,48 @@ test_search_for_asset_snapshot() {
}
run_test_case test_search_for_asset_snapshot
+
+# verify 256 digest of a file
+test_hash_sha256() {
+ target=./test-fixtures/assets/valid/grype_0.78.0_linux_arm64.tar.gz
+
+ # hash_sha256 [target]
+
+ # positive case
+ actual=$(hash_sha256 "${target}")
+ assertEquals "8d57abb57a0dae3ff23c8f0df1f51951b7772822e0d560e860d6f68c24ef6d3d" "${actual}" "mismatched checksum"
+}
+
+run_test_case test_hash_sha256
+
+# verify 256 digest of a file relative to the checksums file
+test_hash_sha256_verify() {
+
+ # hash_sha256_verify [target] [checksums]
+
+
+ # positive case
+
+ checksums=./test-fixtures/assets/valid/checksums.txt
+ target=./test-fixtures/assets/valid/grype_0.78.0_linux_arm64.tar.gz
+
+ hash_sha256_verify "${target}" "${checksums}"
+ assertEquals "0" "$?" "mismatched checksum"
+
+
+ # negative case
+
+ # we are expecting error messages, which is confusing to look at in passing tests... disable logging for now
+ log_set_priority -1
+
+ checksums=./test-fixtures/assets/invalid/checksums.txt
+ target=./test-fixtures/assets/invalid/grype_0.78.0_linux_arm64.tar.gz
+
+ hash_sha256_verify "${target}" "${checksums}"
+ assertEquals "1" "$?" "verification did not catch mismatched checksum"
+
+ # restore logging...
+ log_set_priority 0
+}
+
+run_test_case test_hash_sha256_verify
diff --git a/test/install/2_download_release_asset_test.sh b/test/install/2_download_release_asset_test.sh
index 614a73f5455..075e0acb0d9 100755
--- a/test/install/2_download_release_asset_test.sh
+++ b/test/install/2_download_release_asset_test.sh
@@ -31,6 +31,10 @@ test_download_release_asset() {
release=$(get_release_tag "${OWNER}" "${REPO}" "latest" )
# exercise all possible assets against a real github release (based on asset listing from https://github.com/anchore/grype/releases/tag/v0.32.0)
+
+# verify all downloads against the checksums file + checksums file signature
+VERIFY_SIGN=true
+
run_test_case test_download_release_asset "${release}" "darwin" "amd64" "tar.gz" "application/gzip"
run_test_case test_download_release_asset "${release}" "darwin" "arm64" "tar.gz" "application/gzip"
run_test_case test_download_release_asset "${release}" "linux" "amd64" "tar.gz" "application/gzip"
diff --git a/test/install/3_install_asset_test.sh b/test/install/3_install_asset_test.sh
index d2699232383..8bbaec7872c 100755
--- a/test/install/3_install_asset_test.sh
+++ b/test/install/3_install_asset_test.sh
@@ -32,8 +32,18 @@ test_positive_snapshot_install_asset() {
arch="amd64_v1"
fi
+ local_suffix=""
+ if [ "${arch}" == "arm64" ]; then
+ local_suffix="_v8.0"
+ fi
+
+ if [ "${arch}" == "ppc64le" ]; then
+ local_suffix="_power8"
+ fi
+
+
assertFilesEqual \
- "$(snapshot_dir)/${os}-build_${os}_${arch}/${binary}" \
+ "$(snapshot_dir)/${os}-build_${os}_${arch}${local_suffix}/${binary}" \
"${expected_path}" \
"unable to verify installation of os=${os} arch=${arch} format=${format}"
diff --git a/test/install/4_prep_signature_verification_test.sh b/test/install/4_prep_signature_verification_test.sh
new file mode 100755
index 00000000000..6cc77e25b5d
--- /dev/null
+++ b/test/install/4_prep_signature_verification_test.sh
@@ -0,0 +1,89 @@
+. test_harness.sh
+
+test_compare_semver() {
+ # compare_semver [version1] [version2]
+
+ # positive cases (version1 >= version2)
+ compare_semver "0.32.0" "0.32.0"
+ assertEquals "0" "$?" "+ versions should equal"
+
+ compare_semver "0.32.1" "0.32.0"
+ assertEquals "0" "$?" "+ patch version should be greater"
+
+ compare_semver "0.33.0" "0.32.0"
+ assertEquals "0" "$?" "+ minor version should be greater"
+
+ compare_semver "0.333.0" "0.32.0"
+ assertEquals "0" "$?" "+ minor version should be greater (different length)"
+
+ compare_semver "00.33.00" "0.032.0"
+ assertEquals "0" "$?" "+ minor version should be greater (different length reversed)"
+
+ compare_semver "1.0.0" "0.9.9"
+ assertEquals "0" "$?" "+ major version should be greater"
+
+ compare_semver "v1.0.0" "1.0.0"
+ assertEquals "0" "$?" "+ can remove leading 'v' from version"
+
+ # negative cases (version1 < version2)
+ compare_semver "0.32.0" "0.32.1"
+ assertEquals "1" "$?" "- patch version should be less"
+
+ compare_semver "0.32.7" "0.33.0"
+ assertEquals "1" "$?" "- minor version should be less"
+
+ compare_semver "00.00032.070" "0.33.0"
+ assertEquals "1" "$?" "- minor version should be less (different length)"
+
+ compare_semver "0.32.7" "00.0033.000"
+ assertEquals "1" "$?" "- minor version should be less (different length reversed)"
+
+ compare_semver "1.9.9" "2.0.1"
+ assertEquals "1" "$?" "- major version should be less"
+
+ compare_semver "1.0.0" "v2.0.0"
+ assertEquals "1" "$?" "- can remove leading 'v' from version"
+}
+
+run_test_case test_compare_semver
+
+# ensure that various signature verification pre-requisites are correctly checked for
+test_prep_signature_verification() {
+ # prep_sign_verification [version]
+
+ # we are expecting error messages, which is confusing to look at in passing tests... disable logging for now
+ log_set_priority -1
+
+ # backup original values...
+ OG_COSIGN_BINARY=${COSIGN_BINARY}
+
+ # check the verification path...
+ VERIFY_SIGN=true
+
+ # release does not support signature verification
+ prep_signature_verification "0.71.0"
+ assertEquals "1" "$?" "release does not support signature verification"
+
+ # check that the COSIGN binary exists
+ COSIGN_BINARY=fake-cosign-that-doesnt-exist
+ prep_signature_verification "0.80.0"
+ assertEquals "1" "$?" "cosign binary verification failed"
+ # restore original values...
+ COSIGN_BINARY=${OG_COSIGN_BINARY}
+
+ # ignore any failing conditions since we are not verifying the signature
+ VERIFY_SIGN=false
+ prep_signature_verification "0.71.0"
+ assertEquals "0" "$?" "release support verification should not have been triggered"
+
+ COSIGN_BINARY=fake-cosign-that-doesnt-exist
+ prep_signature_verification "0.80.0"
+ assertEquals "0" "$?" "cosign binary verification should not have been triggered"
+ # restore original values...
+ COSIGN_BINARY=${OG_COSIGN_BINARY}
+
+ # restore logging...
+ log_set_priority 0
+}
+
+run_test_case test_prep_signature_verification
diff --git a/test/install/Makefile b/test/install/Makefile
index 30e002e5bdd..e4c74139f72 100644
--- a/test/install/Makefile
+++ b/test/install/Makefile
@@ -1,20 +1,25 @@
NAME=grype
+# for local testing (not testing within containers) use the binny-managed version of cosign.
+# this also means that the user does not need to install cosign on their system to run tests.
+COSIGN_BINARY=../../.tool/cosign
+
IMAGE_NAME=$(NAME)-install.sh-env
UBUNTU_IMAGE=$(IMAGE_NAME):ubuntu-20.04
ALPINE_IMAGE=$(IMAGE_NAME):alpine-3.6
-BUSYBOX_IMAGE=busybox:1.35
+BUSYBOX_IMAGE=$(IMAGE_NAME):busybox-1.36
ENVS=./environments
DOCKER_RUN=docker run --rm -t -w /project/test/install -v $(shell pwd)/../../:/project
-UNIT=make unit-local
+UNIT=make unit-run
# acceptance testing is running the current install.sh against the latest release. Note: this could be a problem down
# the line if there are breaking changes made that don't align with the latest release (but will be OK with the next
-# release)
-ACCEPTANCE_CMD=sh -c '../../install.sh -b /usr/local/bin && grype version'
+# release). This tests both installing with signature verification and without.
+ACCEPTANCE_CMD=sh -c '../../install.sh -v -b /usr/local/bin && grype version && rm /usr/local/bin/grype && ../../install.sh -b /usr/local/bin && grype version'
# we also want to test against a previous release to ensure that install.sh defers execution to a former install.sh
-PREVIOUS_RELEASE=v0.24.0
+# this version should be at least as recent as when grype was publishing for darwin arm64 as that is what the github runner uses for osx validation
+PREVIOUS_RELEASE=v0.60.0
ACCEPTANCE_PREVIOUS_RELEASE_CMD=sh -c "../../install.sh -b /usr/local/bin $(PREVIOUS_RELEASE) && grype version"
# CI cache busting values; change these if you want CI to not use previous stored cache
@@ -28,17 +33,22 @@ endef
test: unit acceptance
.PHONY: ci-test-mac
-ci-test-mac: unit-local acceptance-local
+ci-test-mac: unit-run acceptance-local
# note: do not add acceptance-local to this list
.PHONY: acceptance
-acceptance: acceptance-ubuntu-20.04 acceptance-alpine-3.6 acceptance-busybox-1.35
+acceptance: acceptance-ubuntu-20.04 acceptance-alpine-3.6 acceptance-busybox-1.36
.PHONY: unit
unit: unit-ubuntu-20.04
.PHONY: unit-local
unit-local:
+ $(call title,unit tests)
+ @for f in $(shell ls *_test.sh); do echo "Running unit test suite '$${f}'"; bash -c "COSIGN_BINARY=$(COSIGN_BINARY) ./$${f}" || exit 1; done
+
+.PHONY: unit-run
+unit-run:
$(call title,unit tests)
@for f in $(shell ls *_test.sh); do echo "Running unit test suite '$${f}'"; bash $${f} || exit 1; done
@@ -55,7 +65,7 @@ acceptance-previous-release-local:
grype version | grep $(shell echo $(PREVIOUS_RELEASE)| tr -d "v")
.PHONY: save
-save: ubuntu-20.04 alpine-3.6 busybox-1.35
+save: ubuntu-20.04 alpine-3.6 busybox-1.36
@mkdir cache || true
docker image save -o cache/ubuntu-env.tar $(UBUNTU_IMAGE)
docker image save -o cache/alpine-env.tar $(ALPINE_IMAGE)
@@ -107,17 +117,17 @@ alpine-3.6:
# note: busybox by default will not have cacerts, so you will get TLS warnings (we want to test under these conditions)
-.PHONY: acceptance-busybox-1.35
-acceptance-busybox-1.35: busybox-1.35
- $(call title,busybox-1.35 - acceptance)
+.PHONY: acceptance-busybox-1.36
+acceptance-busybox-1.36: busybox-1.36
+ $(call title,busybox-1.36 - acceptance)
$(DOCKER_RUN) $(BUSYBOX_IMAGE) \
$(ACCEPTANCE_CMD)
@echo "\n*** test note: you should see grype spit out a 'x509: certificate signed by unknown authority' error --this is expected ***"
-.PHONY: busybox-1.35
-busybox-1.35:
- $(call title,busybox-1.35 - build environment)
- docker pull $(BUSYBOX_IMAGE)
+.PHONY: busybox-1.36
+busybox-1.36:
+ $(call title,busybox-1.36 - build environment)
+ docker build -t $(BUSYBOX_IMAGE) -f $(ENVS)/Dockerfile-busybox-1.36 .
## For CI ########################################################
diff --git a/test/install/environments/Dockerfile-alpine-3.6 b/test/install/environments/Dockerfile-alpine-3.6
index 982e5402996..51dc3d0eadb 100644
--- a/test/install/environments/Dockerfile-alpine-3.6
+++ b/test/install/environments/Dockerfile-alpine-3.6
@@ -1,2 +1,5 @@
FROM alpine:3.6
-RUN apk update && apk add python3 wget unzip make ca-certificates
\ No newline at end of file
+RUN apk update && apk add python3 wget curl unzip make ca-certificates
+RUN curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64" && \
+ mv cosign-linux-amd64 /usr/local/bin/cosign && \
+ chmod +x /usr/local/bin/cosign
diff --git a/test/install/environments/Dockerfile-busybox-1.36 b/test/install/environments/Dockerfile-busybox-1.36
new file mode 100644
index 00000000000..e2cf2d7224a
--- /dev/null
+++ b/test/install/environments/Dockerfile-busybox-1.36
@@ -0,0 +1,21 @@
+FROM alpine as certs
+RUN apk update && apk add ca-certificates
+
+# note: using qemu with a multi-arch image results in redirects not working with wget
+# so let docker pull the image that matches the hosts architecture first and then pull the correct asset
+FROM busybox:1.36.1-musl
+
+RUN ARCH=$(uname -m) && \
+ if [ "$ARCH" = "x86_64" ]; then \
+ COSIGN_ARCH="amd64"; \
+ elif [ "$ARCH" = "aarch64" ]; then \
+ COSIGN_ARCH="arm64"; \
+ else \
+ echo "Unsupported architecture: $ARCH" && exit 1; \
+ fi && \
+ echo "Downloading cosign for $COSIGN_ARCH" && \
+ wget https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-${COSIGN_ARCH} && \
+ mv cosign-linux-${COSIGN_ARCH} /bin/cosign && \
+ chmod +x /bin/cosign
+
+COPY --from=certs /etc/ssl/certs /etc/ssl/certs
diff --git a/test/install/environments/Dockerfile-ubuntu-20.04 b/test/install/environments/Dockerfile-ubuntu-20.04
index dafb64ed73d..25ba033398d 100644
--- a/test/install/environments/Dockerfile-ubuntu-20.04
+++ b/test/install/environments/Dockerfile-ubuntu-20.04
@@ -1,2 +1,5 @@
-FROM ubuntu:20.04
-RUN apt update -y && apt install make python3 curl unzip -y
\ No newline at end of file
+FROM --platform=linux/amd64 ubuntu:20.04@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba
+RUN apt update -y && apt install make python3 curl unzip -y
+RUN LATEST_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d : -f2 | tr -d "v\", ") && \
+ curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign_${LATEST_VERSION}_amd64.deb" && \
+ dpkg -i cosign_${LATEST_VERSION}_amd64.deb
\ No newline at end of file
diff --git a/test/install/test-fixtures/assets/invalid/.gitignore b/test/install/test-fixtures/assets/invalid/.gitignore
new file mode 100644
index 00000000000..967399463e0
--- /dev/null
+++ b/test/install/test-fixtures/assets/invalid/.gitignore
@@ -0,0 +1 @@
+!grype_0.78.0_linux_arm64.tar.gz
\ No newline at end of file
diff --git a/test/install/test-fixtures/assets/invalid/checksums.txt b/test/install/test-fixtures/assets/invalid/checksums.txt
new file mode 100644
index 00000000000..dbf20a0b0d6
--- /dev/null
+++ b/test/install/test-fixtures/assets/invalid/checksums.txt
@@ -0,0 +1,15 @@
+cb4f335e106532b927dac14d4857b7be2333ec1b8bd2aea82be3f9112bb2728f grype_0.78.0_darwin_amd64.tar.gz
+51249ee801b41272218252af2c72a644a7ef037b0b27d7b0eae3b55361e82cf6 grype_0.78.0_darwin_arm64.tar.gz
+cc3cf4fcc856898fcd05ba2b8590de06e380b958fea5957b0a3e4eff5e8aeeaf grype_0.78.0_linux_amd64.deb
+3a9af0f08d1aaf15853f8292be0aa896639e09328416a50d5deaefef894bab61 grype_0.78.0_linux_amd64.rpm
+6037fd3763b6112302b98db559bb5390fbb06f0011c0585a4be03ca851daa838 grype_0.78.0_linux_amd64.tar.gz
+0f2e3e07be5b5eb08637ac9071f4b0f95f8b4c7c7ea66592852ca82fea4adb93 grype_0.78.0_linux_arm64.deb
+89a7f68676a18eb9dc0b706036dacbfb8b78833ed0950b8c6fa63ac159b93781 grype_0.78.0_linux_arm64.rpm
+0d560e860d6f68cf23c8f0df1f5124ef6d3d8d57abb57a0dae3f951b7772822e grype_0.78.0_linux_arm64.tar.gz
+7b22795114e27c3d147998edc9e803988d7c987cad2623d7fb1d7bf730b4e176 grype_0.78.0_linux_ppc64le.deb
+91813ac66ad2ef761ce9629eb4213988de594abd4cab9148a85d71bfa80f6699 grype_0.78.0_linux_ppc64le.rpm
+cb923a08fb9f367410190675f187b6aa5a04c1d538f055700c89c8350b826dcb grype_0.78.0_linux_ppc64le.tar.gz
+4beb9d31d61df6212c3f996fc8f33239520eeea083dbe70b0969f23739d44dd1 grype_0.78.0_linux_s390x.deb
+28f723777b1a136d2fadbdca0ae5e7e9b26f9bd08114095dbd2898def7e8b0b6 grype_0.78.0_linux_s390x.rpm
+6c3e7e54ce40aa33ca5fc774c3be664fb910a99aa77b1e5e3cee77156e8399f4 grype_0.78.0_linux_s390x.tar.gz
+31ca5d02a75dbb8f3361ac9836a2384013a67a7d9e2e437cb80e4ddfbd4c7812 grype_0.78.0_windows_amd64.zip
diff --git a/test/install/test-fixtures/assets/invalid/grype_0.78.0_linux_arm64.tar.gz b/test/install/test-fixtures/assets/invalid/grype_0.78.0_linux_arm64.tar.gz
new file mode 100644
index 00000000000..c90127d2f6c
--- /dev/null
+++ b/test/install/test-fixtures/assets/invalid/grype_0.78.0_linux_arm64.tar.gz
@@ -0,0 +1 @@
+fake archive
\ No newline at end of file
diff --git a/test/install/test-fixtures/assets/valid/.gitignore b/test/install/test-fixtures/assets/valid/.gitignore
new file mode 100644
index 00000000000..967399463e0
--- /dev/null
+++ b/test/install/test-fixtures/assets/valid/.gitignore
@@ -0,0 +1 @@
+!grype_0.78.0_linux_arm64.tar.gz
\ No newline at end of file
diff --git a/test/install/test-fixtures/assets/valid/checksums.txt b/test/install/test-fixtures/assets/valid/checksums.txt
new file mode 100644
index 00000000000..68294b56ba9
--- /dev/null
+++ b/test/install/test-fixtures/assets/valid/checksums.txt
@@ -0,0 +1,15 @@
+cb4f335e106532b927dac14d4857b7be2333ec1b8bd2aea82be3f9112bb2728f grype_0.78.0_darwin_amd64.tar.gz
+51249ee801b41272218252af2c72a644a7ef037b0b27d7b0eae3b55361e82cf6 grype_0.78.0_darwin_arm64.tar.gz
+cc3cf4fcc856898fcd05ba2b8590de06e380b958fea5957b0a3e4eff5e8aeeaf grype_0.78.0_linux_amd64.deb
+3a9af0f08d1aaf15853f8292be0aa896639e09328416a50d5deaefef894bab61 grype_0.78.0_linux_amd64.rpm
+6037fd3763b6112302b98db559bb5390fbb06f0011c0585a4be03ca851daa838 grype_0.78.0_linux_amd64.tar.gz
+0f2e3e07be5b5eb08637ac9071f4b0f95f8b4c7c7ea66592852ca82fea4adb93 grype_0.78.0_linux_arm64.deb
+89a7f68676a18eb9dc0b706036dacbfb8b78833ed0950b8c6fa63ac159b93781 grype_0.78.0_linux_arm64.rpm
+8d57abb57a0dae3ff23c8f0df1f51951b7772822e0d560e860d6f68c24ef6d3d grype_0.78.0_linux_arm64.tar.gz
+7b22795114e27c3d147998edc9e803988d7c987cad2623d7fb1d7bf730b4e176 grype_0.78.0_linux_ppc64le.deb
+91813ac66ad2ef761ce9629eb4213988de594abd4cab9148a85d71bfa80f6699 grype_0.78.0_linux_ppc64le.rpm
+cb923a08fb9f367410190675f187b6aa5a04c1d538f055700c89c8350b826dcb grype_0.78.0_linux_ppc64le.tar.gz
+4beb9d31d61df6212c3f996fc8f33239520eeea083dbe70b0969f23739d44dd1 grype_0.78.0_linux_s390x.deb
+28f723777b1a136d2fadbdca0ae5e7e9b26f9bd08114095dbd2898def7e8b0b6 grype_0.78.0_linux_s390x.rpm
+6c3e7e54ce40aa33ca5fc774c3be664fb910a99aa77b1e5e3cee77156e8399f4 grype_0.78.0_linux_s390x.tar.gz
+31ca5d02a75dbb8f3361ac9836a2384013a67a7d9e2e437cb80e4ddfbd4c7812 grype_0.78.0_windows_amd64.zip
diff --git a/test/install/test-fixtures/assets/valid/grype_0.78.0_linux_arm64.tar.gz b/test/install/test-fixtures/assets/valid/grype_0.78.0_linux_arm64.tar.gz
new file mode 100644
index 00000000000..c90127d2f6c
--- /dev/null
+++ b/test/install/test-fixtures/assets/valid/grype_0.78.0_linux_arm64.tar.gz
@@ -0,0 +1 @@
+fake archive
\ 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 0caed5ac565..eafc94cdbc2 100644
--- a/test/integration/compare_sbom_input_vs_lib_test.go
+++ b/test/integration/compare_sbom_input_vs_lib_test.go
@@ -9,50 +9,41 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/grype/grype"
- "github.com/anchore/grype/grype/db"
- "github.com/anchore/grype/internal"
- "github.com/anchore/syft/syft"
+ "github.com/anchore/grype/grype/db/v6/distribution"
+ "github.com/anchore/grype/grype/db/v6/installation"
+ "github.com/anchore/grype/internal/log"
+ "github.com/anchore/syft/syft/format/spdxjson"
+ "github.com/anchore/syft/syft/format/spdxtagvalue"
+ "github.com/anchore/syft/syft/format/syftjson"
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
-var imagesWithVulnerabilities = []string{
- "anchore/test_images:vulnerabilities-alpine",
- "anchore/test_images:gems",
- "anchore/test_images:vulnerabilities-debian",
- "anchore/test_images:vulnerabilities-centos",
- "anchore/test_images:npm",
- "anchore/test_images:java",
- "anchore/test_images:golang-56d52bc",
- "anchore/test_images:arch",
-}
-
-func getListingURL() string {
+func getLatestURL() string {
if value, ok := os.LookupEnv("GRYPE_DB_UPDATE_URL"); ok {
return value
}
- return internal.DBUpdateURL
+ return distribution.DefaultConfig().LatestURL
}
-func TestCompareSBOMInputToLibResults(t *testing.T) {
- formats := []sbom.FormatID{
- syft.JSONFormatID,
- syft.SPDXJSONFormatID,
- syft.SPDXTagValueFormatID,
+func must(e sbom.FormatEncoder, err error) sbom.FormatEncoder {
+ if err != nil {
+ panic(err)
}
+ return e
+}
+func TestCompareSBOMInputToLibResults(t *testing.T) {
// get a grype DB
- store, _, closer, err := grype.LoadVulnerabilityDB(db.Config{
- DBRootDir: "test-fixtures/grype-db",
- ListingURL: getListingURL(),
- ValidateByHashOnGet: false,
+ store, status, err := grype.LoadVulnerabilityDB(distribution.Config{
+ LatestURL: getLatestURL(),
+ }, installation.Config{
+ DBRootDir: "test-fixtures/grype-db",
+ ValidateChecksum: false,
}, true)
assert.NoError(t, err)
-
- if closer != nil {
- defer closer.Close()
- }
+ defer log.CloseAndLogError(store, status.Path)
definedPkgTypes := strset.New()
for _, p := range syftPkg.AllPkgs {
@@ -60,6 +51,10 @@ func TestCompareSBOMInputToLibResults(t *testing.T) {
}
// exceptions: rust, php, dart, msrc (kb), etc. are not under test
definedPkgTypes.Remove(
+ 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),
@@ -68,60 +63,212 @@ 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),
string(syftPkg.JenkinsPluginPkg), // package type cannot be inferred for all formats
string(syftPkg.LinuxKernelPkg),
string(syftPkg.LinuxKernelModulePkg),
+ string(syftPkg.OpamPkg),
string(syftPkg.Rpkg),
+ string(syftPkg.SwiplPackPkg),
+ string(syftPkg.SwiftPkg),
+ string(syftPkg.GithubActionPkg),
+ string(syftPkg.GithubActionWorkflowPkg),
+ string(syftPkg.GraalVMNativeImagePkg),
+ string(syftPkg.ErlangOTPPkg),
+ string(syftPkg.WordpressPluginPkg), // TODO: remove me when there is a matcher for this merged in https://github.com/anchore/grype/pull/1553
+ string(syftPkg.LuaRocksPkg),
+ string(syftPkg.TerraformPkg),
)
observedPkgTypes := strset.New()
+ testCases := []struct {
+ name string
+ image string
+ format sbom.FormatEncoder
+ }{
+ {
+ image: "anchore/test_images:vulnerabilities-alpine",
+ format: syftjson.NewFormatEncoder(),
+ name: "alpine-syft-json",
+ },
- for _, image := range imagesWithVulnerabilities {
- imageArchive := PullThroughImageCache(t, image)
- imageSource := fmt.Sprintf("docker-archive:%s", imageArchive)
+ {
+ image: "anchore/test_images:vulnerabilities-alpine",
+ format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),
+ name: "alpine-spdx-json",
+ },
- for _, formatID := range formats {
- f := syft.FormatByID(formatID)
- if f == nil {
- t.Errorf("Invalid formatID: %s", formatID)
- }
- t.Run(fmt.Sprintf("%s/%s", image, formatID), func(t *testing.T) {
-
- // get SBOM from syft, write to temp file
- sbomBytes := getSyftSBOM(t, imageSource, f)
- sbomFile, err := os.CreateTemp("", "")
- assert.NoError(t, err)
- t.Cleanup(func() {
- assert.NoError(t, os.Remove(sbomFile.Name()))
- })
- _, err = sbomFile.WriteString(sbomBytes)
- assert.NoError(t, err)
- assert.NoError(t, sbomFile.Close())
-
- // get vulns (sbom)
- matchesFromSbom, _, pkgsFromSbom, err := grype.FindVulnerabilities(*store, fmt.Sprintf("sbom:%s", sbomFile.Name()), source.SquashedScope, nil)
- assert.NoError(t, err)
-
- // get vulns (image)
- matchesFromImage, _, _, err := grype.FindVulnerabilities(*store, imageSource, source.SquashedScope, nil)
- assert.NoError(t, err)
-
- // compare packages (shallow)
- matchSetFromSbom := getMatchSet(matchesFromSbom)
- matchSetFromImage := getMatchSet(matchesFromImage)
-
- assert.Empty(t, strset.Difference(matchSetFromSbom, matchSetFromImage).List(), "vulnerabilities present only in results when using sbom as input")
- assert.Empty(t, strset.Difference(matchSetFromImage, matchSetFromSbom).List(), "vulnerabilities present only in results when using image as input")
-
- // track all covered package types (for use after the test)
- for _, p := range pkgsFromSbom {
- observedPkgTypes.Add(string(p.Type))
- }
+ {
+ image: "anchore/test_images:vulnerabilities-alpine",
+ format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),
+ name: "alpine-spdx-tag-value",
+ },
+
+ {
+ image: "anchore/test_images:gems",
+ format: syftjson.NewFormatEncoder(),
+ name: "gems-syft-json",
+ },
+
+ {
+ image: "anchore/test_images:gems",
+ format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),
+ name: "gems-spdx-json",
+ },
+
+ {
+ image: "anchore/test_images:gems",
+ format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),
+ name: "gems-spdx-tag-value",
+ },
+
+ {
+ image: "anchore/test_images:vulnerabilities-debian",
+ format: syftjson.NewFormatEncoder(),
+ name: "debian-syft-json",
+ },
+
+ {
+ image: "anchore/test_images:vulnerabilities-debian",
+ format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),
+ name: "debian-spdx-json",
+ },
+
+ {
+ image: "anchore/test_images:vulnerabilities-debian",
+ format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),
+ name: "debian-spdx-tag-value",
+ },
+
+ {
+ image: "anchore/test_images:vulnerabilities-centos",
+ format: syftjson.NewFormatEncoder(),
+ name: "centos-syft-json",
+ },
+
+ {
+ image: "anchore/test_images:vulnerabilities-centos",
+ format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),
+ name: "centos-spdx-json",
+ },
+
+ {
+ image: "anchore/test_images:vulnerabilities-centos",
+ format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),
+ name: "centos-spdx-tag-value",
+ },
+
+ {
+ image: "anchore/test_images:npm",
+ format: syftjson.NewFormatEncoder(),
+ name: "npm-syft-json",
+ },
+
+ {
+ image: "anchore/test_images:npm",
+ format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),
+ name: "npm-spdx-json",
+ },
+ {
+ image: "anchore/test_images:npm",
+ format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),
+ name: "npm-spdx-tag-value",
+ },
+
+ {
+ image: "anchore/test_images:java",
+ format: syftjson.NewFormatEncoder(),
+ name: "java-syft-json",
+ },
+
+ {
+ image: "anchore/test_images:java",
+ format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),
+ name: "java-spdx-json",
+ },
+
+ {
+ image: "anchore/test_images:java",
+ format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),
+ name: "java-spdx-tag-value",
+ },
+
+ {
+ image: "anchore/test_images:golang-56d52bc",
+ format: syftjson.NewFormatEncoder(),
+ name: "go-syft-json",
+ },
+
+ {
+ image: "anchore/test_images:golang-56d52bc",
+ format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),
+ name: "go-spdx-json",
+ },
+
+ {
+ image: "anchore/test_images:golang-56d52bc",
+ format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),
+ name: "go-spdx-tag-value",
+ },
+
+ {
+ image: "anchore/test_images:arch",
+ format: syftjson.NewFormatEncoder(),
+ name: "arch-syft-json",
+ },
+
+ {
+ image: "anchore/test_images:arch",
+ format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),
+ name: "arch-spdx-json",
+ },
+
+ {
+ image: "anchore/test_images:arch",
+ format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),
+ name: "arch-spdx-tag-value",
+ },
+ }
+ for _, tc := range testCases {
+ imageArchive := PullThroughImageCache(t, tc.image)
+
+ t.Run(tc.name, func(t *testing.T) {
+ // get SBOM from syft, write to temp file
+ sbomBytes := getSyftSBOM(t, imageArchive, "docker-archive", tc.format)
+ sbomFile, err := os.CreateTemp("", "")
+ assert.NoError(t, err)
+ t.Cleanup(func() {
+ assert.NoError(t, os.Remove(sbomFile.Name()))
})
- }
+ _, err = sbomFile.WriteString(sbomBytes)
+ assert.NoError(t, err)
+ assert.NoError(t, sbomFile.Close())
+
+ // get vulns (sbom)
+ matchesFromSbom, _, pkgsFromSbom, err := grype.FindVulnerabilities(store, fmt.Sprintf("sbom:%s", sbomFile.Name()), source.SquashedScope, nil)
+ assert.NoError(t, err)
+
+ // get vulns (image)
+ imageSource := fmt.Sprintf("docker-archive:%s", imageArchive)
+ matchesFromImage, _, _, err := grype.FindVulnerabilities(store, imageSource, source.SquashedScope, nil)
+ assert.NoError(t, err)
+
+ // compare packages (shallow)
+ matchSetFromSbom := getMatchSet(matchesFromSbom)
+ matchSetFromImage := getMatchSet(matchesFromImage)
+
+ assert.Empty(t, strset.Difference(matchSetFromSbom, matchSetFromImage).List(), "vulnerabilities present only in results when using sbom as input")
+ assert.Empty(t, strset.Difference(matchSetFromImage, matchSetFromSbom).List(), "vulnerabilities present only in results when using image as input")
+
+ // track all covered package types (for use after the test)
+ for _, p := range pkgsFromSbom {
+ observedPkgTypes.Add(string(p.Type))
+ }
+
+ })
}
// ensure we've covered all package types (-rust, -kb)
diff --git a/test/integration/db_mock_test.go b/test/integration/db_mock_test.go
index 716d8202950..9aef8bb2140 100644
--- a/test/integration/db_mock_test.go
+++ b/test/integration/db_mock_test.go
@@ -1,229 +1,224 @@
package integration
import (
- grypeDB "github.com/anchore/grype/grype/db/v5"
+ "github.com/anchore/grype/grype/version"
+ "github.com/anchore/grype/grype/vulnerability"
+ "github.com/anchore/grype/grype/vulnerability/mock"
+ "github.com/anchore/syft/syft/cpe"
)
-// integrity check
-var _ grypeDB.VulnerabilityStoreReader = &mockStore{}
-
-type mockStore struct {
- normalizedPackageNames map[string]map[string]string
- backend map[string]map[string][]grypeDB.Vulnerability
-}
-
-func (s *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
- //TODO implement me
- panic("implement me")
-}
-
-func (s *mockStore) GetVulnerabilityNamespaces() ([]string, error) {
- var results []string
- for k := range s.backend {
- results = append(results, k)
- }
-
- return results, nil
-}
-
-func (s *mockStore) GetVulnerabilityMatchExclusion(id string) ([]grypeDB.VulnerabilityMatchExclusion, error) {
- return nil, nil
-}
-
-func newMockDbStore() *mockStore {
- return &mockStore{
- normalizedPackageNames: map[string]map[string]string{
- "github:language:python": {
- "Pygments": "pygments",
- "my-package": "my-package",
- },
- "github:language:dotnet": {
- "AWSSDK.Core": "awssdk.core",
- },
- },
- backend: map[string]map[string][]grypeDB.Vulnerability{
- "nvd:cpe": {
- "libvncserver": []grypeDB.Vulnerability{
- {
- ID: "CVE-alpine-libvncserver",
- VersionConstraint: "< 0.9.10",
- VersionFormat: "unknown",
- CPEs: []string{"cpe:2.3:a:lib_vnc_project-(server):libvncserver:*:*:*:*:*:*:*:*"},
- },
- },
- "my-package": []grypeDB.Vulnerability{
- {
- ID: "CVE-bogus-my-package-1",
- VersionConstraint: "< 2.0",
- VersionFormat: "unknown",
- CPEs: []string{"cpe:2.3:a:bogus:my-package:*:*:*:*:*:*:something:*"},
- },
- {
- ID: "CVE-bogus-my-package-2-never-match",
- VersionConstraint: "< 2.0",
- VersionFormat: "unknown",
- CPEs: []string{"cpe:2.3:a:something-wrong:my-package:*:*:*:*:*:*:something:*"},
- },
- },
- },
- "alpine:distro:alpine:3.12": {
- "libvncserver": []grypeDB.Vulnerability{
- {
- ID: "CVE-alpine-libvncserver",
- VersionConstraint: "< 0.9.10",
- VersionFormat: "unknown",
- },
- },
- },
- "gentoo:distro:gentoo:2.8": {
- "app-containers/skopeo": []grypeDB.Vulnerability{
- {
- ID: "CVE-gentoo-skopeo",
- VersionConstraint: "< 1.6.0",
- VersionFormat: "unknown",
- },
- },
- },
- "github:language:go": {
- "github.com/anchore/coverage": []grypeDB.Vulnerability{
- {
- ID: "CVE-coverage-main-module-vuln",
- VersionConstraint: "< 1.4.0",
- VersionFormat: "unknown",
- },
- },
- "github.com/google/uuid": []grypeDB.Vulnerability{
- {
- ID: "CVE-uuid-vuln",
- VersionConstraint: "< 1.4.0",
- VersionFormat: "unknown",
- },
- },
- },
- "github:language:javascript": {
- "npm": []grypeDB.Vulnerability{
- {
- ID: "CVE-javascript-validator",
- VersionConstraint: "> 5, < 7.2.1",
- VersionFormat: "unknown",
- },
- },
- },
- "github:language:python": {
- "pygments": []grypeDB.Vulnerability{
- {
- ID: "CVE-python-pygments",
- VersionConstraint: "< 2.6.2",
- VersionFormat: "python",
- },
- },
- "my-package": []grypeDB.Vulnerability{
- {
- ID: "CVE-bogus-my-package-2-python",
- VersionConstraint: "< 2.0",
- VersionFormat: "python",
- },
- },
- },
- "github:language:ruby": {
- "bundler": []grypeDB.Vulnerability{
- {
- ID: "CVE-ruby-bundler",
- VersionConstraint: "> 2.0.0, <= 2.1.4",
- VersionFormat: "gemfile",
- },
- },
- },
- "github:language:java": {
- "org.anchore:example-java-app-maven": []grypeDB.Vulnerability{
- {
- ID: "CVE-java-example-java-app",
- VersionConstraint: ">= 0.0.1, < 1.2.0",
- VersionFormat: "unknown",
- },
- },
- },
- "github:language:dotnet": {
- "awssdk.core": []grypeDB.Vulnerability{
- {
- ID: "CVE-dotnet-sample",
- VersionConstraint: ">= 3.7.0.0, < 3.7.12.0",
- VersionFormat: "dotnet",
- },
- },
- },
- "github:language:haskell": {
- "shellcheck": []grypeDB.Vulnerability{
- {
- ID: "CVE-haskell-sample",
- VersionConstraint: "< 0.9.0",
- VersionFormat: "haskell",
- },
- },
- },
- "debian:distro:debian:8": {
- "apt-dev": []grypeDB.Vulnerability{
- {
- ID: "CVE-dpkg-apt",
- VersionConstraint: "<= 1.8.2",
- VersionFormat: "dpkg",
- },
- },
- },
- "redhat:distro:redhat:8": {
- "dive": []grypeDB.Vulnerability{
- {
- ID: "CVE-rpmdb-dive",
- VersionConstraint: "<= 1.0.42",
- VersionFormat: "rpm",
- },
- },
- },
- "msrc:distro:windows:10816": {
- "10816": []grypeDB.Vulnerability{
- {
- ID: "CVE-2016-3333",
- VersionConstraint: "3200970 || 878787 || base",
- VersionFormat: "kb",
- },
- },
- },
- "sles:distro:sles:12.5": {
- "dive": []grypeDB.Vulnerability{
- {
- ID: "CVE-rpmdb-dive",
- VersionConstraint: "<= 1.0.42",
- VersionFormat: "rpm",
- },
- },
- },
- },
- }
-}
-
-func (s *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
- namespaceMap := s.backend[namespace]
- if namespaceMap == nil {
- return nil, nil
- }
- entries, ok := namespaceMap[name]
- if !ok {
- return entries, nil
- }
- for i := range entries {
- entries[i].Namespace = namespace
- }
- return entries, nil
-}
-
-func (s *mockStore) GetAllVulnerabilities() (*[]grypeDB.Vulnerability, error) {
- return nil, nil
-}
-
-func (s *mockStore) GetVulnerabilityMetadata(id string, namespace string) (*grypeDB.VulnerabilityMetadata, error) {
- return nil, nil
-}
-
-func (s *mockStore) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) {
- return nil, nil
+func newMockDbProvider() vulnerability.Provider {
+ return mock.VulnerabilityProvider([]vulnerability.Vulnerability{
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-jdk",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "jdk",
+ Constraint: version.MustGetConstraint("< 1.8.0_401", version.JVMFormat),
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", "")},
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-alpine-libvncserver",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("< 0.9.10", version.UnknownFormat),
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:lib_vnc_project-(server):libvncserver:*:*:*:*:*:*:*:*", "")},
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-bogus-my-package-1",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "my-package",
+ Constraint: version.MustGetConstraint("< 2.0", version.UnknownFormat),
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:bogus:my-package:*:*:*:*:*:*:something:*", "")},
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-bogus-my-package-2-never-match",
+ Namespace: "nvd:cpe",
+ },
+ PackageName: "my-package",
+ Constraint: version.MustGetConstraint("< 2.0", version.UnknownFormat),
+ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:something-wrong:my-package:*:*:*:*:*:*:something:*", "")},
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-alpine-libvncserver",
+ Namespace: "alpine:distro:alpine:3.12",
+ },
+ PackageName: "libvncserver",
+ Constraint: version.MustGetConstraint("< 0.9.10", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-azure-autorest-vuln-false-positive",
+ Namespace: "alpine:distro:alpine:3.12",
+ },
+ PackageName: "ko",
+ Constraint: version.MustGetConstraint("< 0", version.ApkFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-npm-false-positive-in-apk-subpackage",
+ Namespace: "alpine:distro:alpine:3.12",
+ },
+ PackageName: "npm-apk-package-with-false-positive",
+ Constraint: version.MustGetConstraint("< 0", version.ApkFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-gentoo-skopeo",
+ Namespace: "gentoo:distro:gentoo:2.8",
+ },
+ PackageName: "app-containers/skopeo",
+ Constraint: version.MustGetConstraint("< 1.6.0", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-coverage-main-module-vuln",
+ Namespace: "github:language:go",
+ },
+ PackageName: "github.com/anchore/coverage",
+ Constraint: version.MustGetConstraint("< 1.4.0", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-uuid-vuln",
+ Namespace: "github:language:go",
+ },
+ PackageName: "github.com/google/uuid",
+ Constraint: version.MustGetConstraint("< 1.4.0", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-azure-autorest-vuln-false-positive",
+ Namespace: "github:language:go",
+ },
+ PackageName: "github.com/azure/go-autorest/autorest",
+ Constraint: version.MustGetConstraint("< 0.11.30", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-bogus-my-package-2-idris",
+ Namespace: "github:language:idris",
+ },
+ PackageName: "my-package",
+ Constraint: version.MustGetConstraint("< 2.0", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-javascript-validator",
+ Namespace: "github:language:javascript",
+ },
+ PackageName: "npm",
+ Constraint: version.MustGetConstraint("> 5, < 7.2.1", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-npm-false-positive-in-apk-subpackage",
+ Namespace: "github:language:javascript",
+ },
+ PackageName: "npm-apk-subpackage-with-false-positive",
+ Constraint: version.MustGetConstraint("< 2.0.0", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-python-pygments",
+ Namespace: "github:language:python",
+ },
+ PackageName: "pygments",
+ Constraint: version.MustGetConstraint("< 2.6.2", version.PythonFormat),
+ },
+ //{
+ // Reference: vulnerability.Reference{
+ // ID: "CVE-my-package-python",
+ // Namespace: "github:language:python",
+ // },
+ // PackageName: "my-package",
+ //},
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-ruby-bundler",
+ Namespace: "github:language:ruby", // github:language:gem ??
+ },
+ PackageName: "bundler",
+ Constraint: version.MustGetConstraint("> 2.0.0, <= 2.1.4", version.UnknownFormat), //version.GemFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-java-example-java-app",
+ Namespace: "github:language:java",
+ },
+ PackageName: "org.anchore:example-java-app-maven",
+ Constraint: version.MustGetConstraint(">= 0.0.1, < 1.2.0", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-dotnet-sample",
+ Namespace: "github:language:dotnet",
+ },
+ PackageName: "awssdk.core",
+ Constraint: version.MustGetConstraint(">= 3.7.0.0, < 3.7.12.0", version.UnknownFormat), // was: "dotnet"
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-haskell-sample",
+ Namespace: "github:language:haskell",
+ },
+ PackageName: "shellcheck",
+ Constraint: version.MustGetConstraint("< 0.9.0", version.UnknownFormat), // was: "haskell"
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-rust-sample-1",
+ Namespace: "github:language:rust",
+ },
+ PackageName: "hello-auditable",
+ Constraint: version.MustGetConstraint("< 0.2.0", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-rust-sample-2",
+ Namespace: "github:language:rust",
+ },
+ PackageName: "auditable",
+ Constraint: version.MustGetConstraint("< 0.2.0", version.UnknownFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-dpkg-apt",
+ Namespace: "debian:distro:debian:8",
+ },
+ PackageName: "apt-dev",
+ Constraint: version.MustGetConstraint("<= 1.8.2", version.DebFormat), // was: "dpkg"
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-rpmdb-dive",
+ Namespace: "redhat:distro:redhat:8",
+ },
+ PackageName: "dive",
+ Constraint: version.MustGetConstraint("<= 1.0.42", version.RpmFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-2016-3333",
+ Namespace: "msrc:distro:windows:10816",
+ },
+ PackageName: "10816",
+ Constraint: version.MustGetConstraint("3200970 || 878787 || base", version.KBFormat),
+ },
+ {
+ Reference: vulnerability.Reference{
+ ID: "CVE-rpmdb-dive",
+ Namespace: "sles:distro:sles:12.5",
+ },
+ PackageName: "dive",
+ Constraint: version.MustGetConstraint("<= 1.0.42", version.RpmFormat),
+ },
+ }...)
}
diff --git a/test/integration/diff_test.go b/test/integration/diff_test.go
deleted file mode 100644
index ddda47a09e6..00000000000
--- a/test/integration/diff_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package integration
-
-import (
- "flag"
-)
-
-var update = flag.Bool("update", false, "update the *.golden files for diff presenter")
-
-const (
- baseURL = "https://toolbox-data.anchore.io/grype/staging-databases/vulnerability-db_v5_2022-10-14T08:22:01Z_69c99aa5917dea969f2d.tar.gz"
- targetURL = "https://toolbox-data.anchore.io/grype/staging-databases/vulnerability-db_v5_2022-10-17T08:14:57Z_10e4086061ab36cfa900.tar.gz"
-)
-
-// TODO: Rework this test to not be dependent on hosted DBs. Disabling to get around failures while bumping schema
-
-//func TestDatabaseDiff(t *testing.T) {
-// //GIVEN
-// differ, err := differ.NewDiffer(db.Config{
-// DBRootDir: "test-fixtures/grype-db",
-// ListingURL: getListingURL(),
-// ValidateByHashOnGet: false,
-// })
-// var buffer bytes.Buffer
-// base, err := url.Parse(baseURL)
-// require.NoError(t, err)
-// target, err := url.Parse(targetURL)
-// require.NoError(t, err)
-//
-// //WHEN
-// require.NoError(t, differ.DownloadDatabases(base, target))
-// diffs, err := differ.DiffDatabases()
-// require.NoError(t, err)
-// for i := range *diffs {
-// sort.Strings((*diffs)[i].Packages)
-// }
-// sort.SliceStable(*diffs, func(i, j int) bool {
-// d1, d2 := (*diffs)[i], (*diffs)[j]
-// return (d1.ID + d1.Namespace) < (d2.ID + d2.Namespace)
-// })
-// require.NoError(t, differ.Present("json", diffs, &buffer))
-//
-// //THEN
-// actual := buffer.Bytes()
-// if *update {
-// testutils.UpdateGoldenFileContents(t, actual)
-// }
-// var expected = testutils.GetGoldenFileContents(t)
-// if !bytes.Equal(expected, actual) {
-// dmp := diffmatchpatch.New()
-// diffs := dmp.DiffMain(string(expected), string(actual), true)
-// t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
-// }
-//}
diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go
index b10e3e3c233..9a226cc2157 100644
--- a/test/integration/match_by_image_test.go
+++ b/test/integration/match_by_image_test.go
@@ -1,6 +1,7 @@
package integration
import (
+ "context"
"sort"
"strings"
"testing"
@@ -10,40 +11,51 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/grype/grype"
- "github.com/anchore/grype/grype/db"
+ "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"
+ "github.com/anchore/grype/grype/matcher/golang"
+ "github.com/anchore/grype/grype/matcher/java"
+ "github.com/anchore/grype/grype/matcher/javascript"
+ "github.com/anchore/grype/grype/matcher/python"
+ "github.com/anchore/grype/grype/matcher/ruby"
+ "github.com/anchore/grype/grype/matcher/rust"
+ "github.com/anchore/grype/grype/matcher/stock"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/store"
+ "github.com/anchore/grype/grype/search"
+ "github.com/anchore/grype/grype/vex"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/stringutil"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
+ "github.com/anchore/syft/syft/cataloging/pkgcataloging"
+ "github.com/anchore/syft/syft/cpe"
syftPkg "github.com/anchore/syft/syft/pkg"
- "github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/source"
)
-func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/lib/apk/db/installed")
- if len(packages) != 1 {
+ if len(packages) != 3 {
t.Logf("Alpine Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (alpine)")
}
thePkg := pkg.New(packages[0])
- theVuln := theStore.backend["alpine:distro:alpine:3.12"][thePkg.Name][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("alpine:distro:alpine:3.12"), search.ByPackageName(thePkg.Name))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
// note: we are matching on the secdb record, not NVD primarily
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
- // note: the input pURL has an upstream reference (redundant)
- Type: "exact-indirect-match",
+ Type: match.ExactDirectMatch,
+ Confidence: 1.0,
SearchedBy: map[string]any{
"distro": map[string]string{
"type": "alpine",
@@ -57,15 +69,14 @@ func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
},
Found: map[string]any{
"versionConstraint": "< 0.9.10 (unknown)",
- "vulnerabilityID": "CVE-alpine-libvncserver",
+ "vulnerabilityID": vulnObj.ID,
},
- Matcher: "apk-matcher",
- Confidence: 1,
+ Matcher: match.ApkMatcher,
},
{
- Type: match.ExactDirectMatch,
- Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ // note: the input pURL has an upstream reference (redundant)
+ Type: "exact-indirect-match",
+ SearchedBy: map[string]any{
"distro": map[string]string{
"type": "alpine",
"version": "3.12.0",
@@ -76,35 +87,37 @@ func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
"version": "0.9.9",
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": "< 0.9.10 (unknown)",
- "vulnerabilityID": vulnObj.ID,
+ "vulnerabilityID": "CVE-alpine-libvncserver",
},
- Matcher: match.ApkMatcher,
+ Matcher: "apk-matcher",
+ Confidence: 1,
},
},
})
}
-func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/javascript/pkg-json/package.json")
if len(packages) != 1 {
t.Logf("Javascript Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (javascript)")
}
thePkg := pkg.New(packages[0])
- theVuln := theStore.backend["github:language:javascript"][thePkg.Name][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("github:language:javascript"), search.ByPackageName(thePkg.Name))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"language": "javascript",
"namespace": "github:language:javascript",
"package": map[string]string{
@@ -112,7 +125,7 @@ func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPk
"version": thePkg.Version,
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": "> 5, < 7.2.1 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
@@ -122,7 +135,7 @@ func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPk
})
}
-func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/python/dist-info/METADATA")
if len(packages) != 1 {
for _, p := range packages {
@@ -132,20 +145,19 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
t.Fatalf("problem with upstream syft cataloger (python)")
}
thePkg := pkg.New(packages[0])
- normalizedName := theStore.normalizedPackageNames["github:language:python"][thePkg.Name]
- theVuln := theStore.backend["github:language:python"][normalizedName][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("github:language:python"), search.ByPackageName(strings.ToLower(thePkg.Name)))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
-
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"language": "python",
"namespace": "github:language:python",
"package": map[string]string{
@@ -153,7 +165,7 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
"version": thePkg.Version,
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": "< 2.6.2 (python)",
"vulnerabilityID": vulnObj.ID,
},
@@ -163,30 +175,43 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
})
}
-func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+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) != 1 {
+ // 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)
}
t.Fatalf("problem with upstream syft cataloger (dotnet)")
}
- thePkg := pkg.New(packages[0])
- normalizedName := theStore.normalizedPackageNames["github:language:dotnet"][thePkg.Name]
- theVuln := theStore.backend["github:language:dotnet"][normalizedName][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ thePkg := pkg.New(packages[1])
+ vulns, err := provider.FindVulnerabilities(byNamespace("github:language:dotnet"), search.ByPackageName(strings.ToLower(thePkg.Name)))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
-
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"language": "dotnet",
"namespace": "github:language:dotnet",
"package": map[string]string{
@@ -194,7 +219,7 @@ func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
"version": thePkg.Version,
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": ">= 3.7.0.0, < 3.7.12.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
@@ -204,26 +229,26 @@ func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
})
}
-func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/ruby/specifications/bundler.gemspec")
if len(packages) != 1 {
t.Logf("Ruby Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (ruby)")
}
thePkg := pkg.New(packages[0])
- theVuln := theStore.backend["github:language:ruby"][thePkg.Name][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("github:language:ruby"), search.ByPackageName(thePkg.Name))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
-
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"language": "ruby",
"namespace": "github:language:ruby",
"package": map[string]string{
@@ -231,7 +256,7 @@ func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
"version": thePkg.Version,
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": "> 2.0.0, <= 2.1.4 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
@@ -241,7 +266,7 @@ func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
})
}
-func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
modPackages := catalog.PackagesByPath("/golang/go.mod")
if len(modPackages) != 1 {
t.Logf("Golang Mod Packages: %+v", modPackages)
@@ -249,7 +274,8 @@ func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
}
binPackages := catalog.PackagesByPath("/go-app")
- if len(binPackages) != 2 {
+ // contains 2 package + a single stdlib package
+ if len(binPackages) != 3 {
t.Logf("Golang Bin Packages: %+v", binPackages)
t.Fatalf("problem with upstream syft cataloger (golang)")
}
@@ -264,19 +290,24 @@ func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
continue
}
+ if p.Name == "stdlib" {
+ continue
+ }
+
thePkg := pkg.New(p)
- theVuln := theStore.backend["github:language:go"][thePkg.Name][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("github:language:go"), search.ByPackageName(thePkg.Name))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"language": "go",
"namespace": "github:language:go",
"package": map[string]string{
@@ -284,7 +315,7 @@ func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
"version": thePkg.Version,
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": "< 1.4.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
@@ -296,7 +327,7 @@ func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co
}
}
-func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := make([]syftPkg.Package, 0)
for p := range catalog.Enumerate(syftPkg.JavaPkg) {
packages = append(packages, p)
@@ -307,23 +338,23 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
}
theSyftPkg := packages[0]
- groupId := theSyftPkg.Metadata.(syftPkg.JavaMetadata).PomProperties.GroupID
+ groupId := theSyftPkg.Metadata.(syftPkg.JavaArchive).PomProperties.GroupID
lookup := groupId + ":" + theSyftPkg.Name
thePkg := pkg.New(theSyftPkg)
-
- theVuln := theStore.backend["github:language:java"][lookup][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("github:language:java"), search.ByPackageName(lookup))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"language": "java",
"namespace": "github:language:java",
"package": map[string]string{
@@ -331,7 +362,7 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
"version": thePkg.Version,
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": ">= 0.0.1, < 1.2.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
@@ -341,7 +372,7 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
})
}
-func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/var/lib/dpkg/status")
if len(packages) != 1 {
t.Logf("Dpkg Packages: %+v", packages)
@@ -349,19 +380,19 @@ func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
}
thePkg := pkg.New(packages[0])
// NOTE: this is an indirect match, in typical debian style
- theVuln := theStore.backend["debian:distro:debian:8"][thePkg.Name+"-dev"][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("debian:distro:debian:8"), search.ByPackageName(thePkg.Name+"-dev"))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
-
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactIndirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"distro": map[string]string{
"type": "debian",
"version": "8",
@@ -372,7 +403,7 @@ func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
"version": "1.8.2",
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": "<= 1.8.2 (deb)",
"vulnerabilityID": vulnObj.ID,
},
@@ -382,25 +413,26 @@ func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
})
}
-func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS")
if len(packages) != 1 {
t.Logf("Portage Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (portage)")
}
thePkg := pkg.New(packages[0])
- theVuln := theStore.backend["gentoo:distro:gentoo:2.8"][thePkg.Name][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("gentoo:distro:gentoo:2.8"), search.ByPackageName(thePkg.Name))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"distro": map[string]string{
"type": "gentoo",
"version": "2.8",
@@ -411,7 +443,7 @@ func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C
"version": "1.5.1",
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": "< 1.6.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
@@ -421,26 +453,26 @@ func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C
})
}
-func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/var/lib/rpm/Packages")
if len(packages) != 1 {
t.Logf("RPMDB Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (RPMDB)")
}
thePkg := pkg.New(packages[0])
- theVuln := theStore.backend["redhat:distro:redhat:8"][thePkg.Name][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("redhat:distro:redhat:8"), search.ByPackageName(thePkg.Name))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
-
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"distro": map[string]string{
"type": "centos",
"version": "8",
@@ -451,7 +483,7 @@ func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
"version": "0:0.9.2-1",
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": "<= 1.0.42 (rpm)",
"vulnerabilityID": vulnObj.ID,
},
@@ -461,26 +493,28 @@ func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
})
}
-func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/var/lib/rpm/Packages")
if len(packages) != 1 {
t.Logf("Sles Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (RPMDB)")
}
thePkg := pkg.New(packages[0])
- theVuln := theStore.backend["redhat:distro:redhat:8"][thePkg.Name][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+
+ vulns, err := provider.FindVulnerabilities(byNamespace("redhat:distro:redhat:8"), search.ByPackageName(thePkg.Name))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
vulnObj.Namespace = "sles:distro:sles:12.5"
theResult.Add(match.Match{
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
- SearchedBy: map[string]interface{}{
+ SearchedBy: map[string]any{
"distro": map[string]string{
"type": "sles",
"version": "12.5",
@@ -491,7 +525,7 @@ func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
"version": "0:0.9.2-1",
},
},
- Found: map[string]interface{}{
+ Found: map[string]any{
"versionConstraint": "<= 1.0.42 (rpm)",
"vulnerabilityID": vulnObj.ID,
},
@@ -501,19 +535,20 @@ func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll
})
}
-func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
+func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/haskell/stack.yaml")
if len(packages) < 1 {
- t.Logf("Haskel Packages: %+v", packages)
+ t.Logf("Haskell Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (haskell)")
}
thePkg := pkg.New(packages[0])
- theVuln := theStore.backend["github:language:haskell"][strings.ToLower(thePkg.Name)][0]
- vulnObj, err := vulnerability.NewVulnerability(theVuln)
+ vulns, err := provider.FindVulnerabilities(byNamespace("github:language:haskell"), search.ByPackageName(strings.ToLower(thePkg.Name)))
require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
theResult.Add(match.Match{
- Vulnerability: *vulnObj,
+ Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
@@ -537,6 +572,93 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C
})
}
+func addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
+ packages := catalog.PackagesByPath("/opt/java/openjdk/release")
+ if len(packages) < 1 {
+ t.Logf("JVM Packages: %+v", packages)
+ t.Fatalf("problem with upstream syft cataloger (java-jvm-cataloger)")
+ }
+
+ for _, p := range packages {
+ thePkg := pkg.New(p)
+ vulns, err := provider.FindVulnerabilities(byNamespace("nvd:cpe"), search.ByPackageName(thePkg.Name))
+ require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
+
+ // why is this being set?
+ vulnObj.CPEs = []cpe.CPE{
+ cpe.Must("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", ""),
+ }
+
+ theResult.Add(match.Match{
+ Vulnerability: vulnObj,
+ Package: thePkg,
+ Details: []match.Detail{
+ {
+ Type: match.CPEMatch,
+ Confidence: 0.9,
+ SearchedBy: match.CPEParameters{
+ Namespace: "nvd:cpe",
+ CPEs: []string{
+ "cpe:2.3:a:oracle:jdk:1.8.0:update400:*:*:*:*:*:*",
+ },
+ Package: match.CPEPackageParameter{Name: "jdk", Version: "1.8.0_400-b07"},
+ },
+ Found: match.CPEResult{
+ VulnerabilityID: "CVE-jdk",
+ VersionConstraint: "< 1.8.0_401 (jvm)",
+ CPEs: []string{
+ "cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*",
+ },
+ },
+ Matcher: match.StockMatcher,
+ },
+ },
+ })
+ }
+}
+
+func addRustMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
+ packages := catalog.PackagesByPath("/hello-auditable")
+ if len(packages) < 1 {
+ t.Logf("Rust Packages: %+v", packages)
+ t.Fatalf("problem with upstream syft cataloger (cargo-auditable-binary-cataloger)")
+ }
+
+ for _, p := range packages {
+ thePkg := pkg.New(p)
+ vulns, err := provider.FindVulnerabilities(byNamespace("github:language:rust"), search.ByPackageName(thePkg.Name))
+ require.NoError(t, err)
+ require.NotEmpty(t, vulns)
+ vulnObj := vulns[0]
+
+ theResult.Add(match.Match{
+ Vulnerability: vulnObj,
+ Package: thePkg,
+ Details: []match.Detail{
+ {
+ Type: match.ExactDirectMatch,
+ Confidence: 1.0,
+ SearchedBy: map[string]any{
+ "language": "rust",
+ "namespace": "github:language:rust",
+ "package": map[string]string{
+ "name": thePkg.Name,
+ "version": thePkg.Version,
+ },
+ },
+ Found: map[string]any{
+ "versionConstraint": vulnObj.Constraint.String(),
+ "vulnerabilityID": vulnObj.ID,
+ },
+ Matcher: match.RustMatcher,
+ },
+ },
+ })
+ }
+}
+
func TestMatchByImage(t *testing.T) {
observedMatchers := stringutil.NewStringSet()
definedMatchers := stringutil.NewStringSet()
@@ -545,101 +667,127 @@ func TestMatchByImage(t *testing.T) {
}
tests := []struct {
- fixtureImage string
- expectedFn func(source.Source, *syftPkg.Collection, *mockStore) match.Matches
+ name string
+ expectedFn func(source.Source, *syftPkg.Collection, vulnerability.Provider) match.Matches
}{
{
- fixtureImage: "image-debian-match-coverage",
- expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches {
+ name: "image-debian-match-coverage",
+ expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {
expectedMatches := match.NewMatches()
- addPythonMatches(t, theSource, catalog, theStore, &expectedMatches)
- addRubyMatches(t, theSource, catalog, theStore, &expectedMatches)
- addJavaMatches(t, theSource, catalog, theStore, &expectedMatches)
- addDpkgMatches(t, theSource, catalog, theStore, &expectedMatches)
- addJavascriptMatches(t, theSource, catalog, theStore, &expectedMatches)
- addDotnetMatches(t, theSource, catalog, theStore, &expectedMatches)
- addGolangMatches(t, theSource, catalog, theStore, &expectedMatches)
- addHaskellMatches(t, theSource, catalog, theStore, &expectedMatches)
+ addPythonMatches(t, theSource, catalog, provider, &expectedMatches)
+ addRubyMatches(t, theSource, catalog, provider, &expectedMatches)
+ addJavaMatches(t, theSource, catalog, provider, &expectedMatches)
+ addDpkgMatches(t, theSource, catalog, provider, &expectedMatches)
+ addJavascriptMatches(t, theSource, catalog, provider, &expectedMatches)
+ addDotnetMatches(t, theSource, catalog, provider, &expectedMatches)
+ addGolangMatches(t, theSource, catalog, provider, &expectedMatches)
+ addHaskellMatches(t, theSource, catalog, provider, &expectedMatches)
return expectedMatches
},
},
{
- fixtureImage: "image-centos-match-coverage",
- expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches {
+ name: "image-centos-match-coverage",
+ expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {
expectedMatches := match.NewMatches()
- addRhelMatches(t, theSource, catalog, theStore, &expectedMatches)
+ addRhelMatches(t, theSource, catalog, provider, &expectedMatches)
return expectedMatches
},
},
{
- fixtureImage: "image-alpine-match-coverage",
- expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches {
+ name: "image-alpine-match-coverage",
+ expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {
expectedMatches := match.NewMatches()
- addAlpineMatches(t, theSource, catalog, theStore, &expectedMatches)
+ addAlpineMatches(t, theSource, catalog, provider, &expectedMatches)
return expectedMatches
},
},
{
- fixtureImage: "image-sles-match-coverage",
- expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches {
+ name: "image-sles-match-coverage",
+ expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {
expectedMatches := match.NewMatches()
- addSlesMatches(t, theSource, catalog, theStore, &expectedMatches)
+ addSlesMatches(t, theSource, catalog, provider, &expectedMatches)
return expectedMatches
},
},
+ // TODO: add this back in when #744 is fully implemented (see https://github.com/anchore/grype/issues/744#issuecomment-2448163737)
+ //{
+ // name: "image-portage-match-coverage",
+ // expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {
+ // expectedMatches := match.NewMatches()
+ // addPortageMatches(t, theSource, catalog, provider, &expectedMatches)
+ // return expectedMatches
+ // },
+ //},
{
- fixtureImage: "image-portage-match-coverage",
- expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches {
+ name: "image-rust-auditable-match-coverage",
+ expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {
expectedMatches := match.NewMatches()
- addPortageMatches(t, theSource, catalog, theStore, &expectedMatches)
+ addRustMatches(t, theSource, catalog, provider, &expectedMatches)
+ return expectedMatches
+ },
+ },
+ {
+ name: "image-jvm-match-coverage",
+ expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {
+ expectedMatches := match.NewMatches()
+ addJvmMatches(t, theSource, catalog, provider, &expectedMatches)
return expectedMatches
},
},
}
for _, test := range tests {
- t.Run(test.fixtureImage, func(t *testing.T) {
- theStore := newMockDbStore()
-
- imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
- tarPath := imagetest.GetFixtureImageTarPath(t, test.fixtureImage)
+ t.Run(test.name, func(t *testing.T) {
+ theProvider := newMockDbProvider()
- userImage := "docker-archive:" + tarPath
-
- detection, err := source.Detect(userImage, source.DetectConfig{})
- require.NoError(t, err)
+ imagetest.GetFixtureImage(t, "docker-archive", test.name)
+ tarPath := imagetest.GetFixtureImageTarPath(t, test.name)
// this is purely done to help setup mocks
- theSource, err := detection.NewSource(source.DetectionSourceConfig{})
+ theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, theSource.Close())
})
// TODO: relationships are not verified at this time
- config := cataloger.DefaultConfig()
- config.Search.Scope = source.SquashedScope
-
// enable all catalogers to cover non default cases
- config.Catalogers = []string{"all"}
+ config := syft.DefaultCreateSBOMConfig().WithCatalogerSelection(pkgcataloging.NewSelectionRequest().WithDefaults("all"))
+ config.Search.Scope = source.SquashedScope
- collection, _, theDistro, err := syft.CatalogPackages(theSource, config)
+ s, err := syft.CreateSBOM(context.Background(), theSource, config)
require.NoError(t, err)
+ require.NotNil(t, s)
- matchers := matcher.NewDefaultMatchers(matcher.Config{})
-
- vp, err := db.NewVulnerabilityProvider(theStore)
- require.NoError(t, err)
- mp := db.NewVulnerabilityMetadataProvider(theStore)
- ep := db.NewMatchExclusionProvider(theStore)
- str := store.Store{
- Provider: vp,
- MetadataProvider: mp,
- ExclusionProvider: ep,
- }
-
- actualResults := grype.FindVulnerabilitiesForPackage(str, theDistro, matchers, pkg.FromCollection(collection, pkg.SynthesisConfig{}))
+ // TODO: we need to use the API default configuration, not something hard coded here
+ matchers := matcher.NewDefaultMatchers(matcher.Config{
+ Java: java.MatcherConfig{
+ UseCPEs: true,
+ },
+ Ruby: ruby.MatcherConfig{
+ UseCPEs: true,
+ },
+ Python: python.MatcherConfig{
+ UseCPEs: true,
+ },
+ Dotnet: dotnet.MatcherConfig{
+ UseCPEs: true,
+ },
+ Javascript: javascript.MatcherConfig{
+ UseCPEs: true,
+ },
+ Golang: golang.MatcherConfig{
+ UseCPEs: true,
+ },
+ Rust: rust.MatcherConfig{
+ UseCPEs: true,
+ },
+ Stock: stock.MatcherConfig{
+ UseCPEs: true,
+ },
+ })
+ 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))
@@ -647,9 +795,58 @@ func TestMatchByImage(t *testing.T) {
}
// build expected matches from what's discovered from the catalog
- expectedMatches := test.expectedFn(theSource, collection, theStore)
+ expectedMatches := test.expectedFn(theSource, s.Artifacts.Packages, theProvider)
+
+ assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted())
+ })
+ }
+
+ // Test that VEX matchers produce matches when fed documents with "affected"
+ // statuses.
+ for n, tc := range map[string]struct {
+ vexStatus vex.Status
+ vexDocuments []string
+ }{
+ "openvex-affected": {vex.StatusAffected, []string{"test-fixtures/vex/openvex/affected.openvex.json"}},
+ "openvex-under_investigation": {vex.StatusUnderInvestigation, []string{"test-fixtures/vex/openvex/under_investigation.openvex.json"}},
+ } {
+ t.Run(n, func(t *testing.T) {
+ ignoredMatches := testIgnoredMatches()
+ vexedResults := vexMatches(t, ignoredMatches, tc.vexStatus, tc.vexDocuments)
+ if len(vexedResults.Sorted()) != 1 {
+ t.Errorf("expected one vexed result, got none")
+ }
+
+ expectedMatches := match.NewMatches()
+
+ // The single match in the actual results is the same in ignoredMatched
+ // but must the details of the VEX matcher appended
+ if len(vexedResults.Sorted()) < 1 {
+ t.Errorf(
+ "Expected VEXed Results to produce an array of vexMatches but got none; len(vexedResults)=%d",
+ len(vexedResults.Sorted()),
+ )
+ }
+ result := vexedResults.Sorted()[0]
+ if len(result.Details) != len(ignoredMatches[0].Match.Details)+1 {
+ t.Errorf(
+ "Details in VEXed results don't match (expected %d, got %d)",
+ len(ignoredMatches[0].Match.Details)+1, len(result.Details),
+ )
+ }
+ result.Details = result.Details[:len(result.Details)-1]
+ actualResults := match.NewMatches()
+ actualResults.Add(result)
+
+ expectedMatches.Add(ignoredMatches[0].Match)
assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted())
+
+ for _, m := range vexedResults.Sorted() {
+ for _, d := range m.Details {
+ observedMatchers.Add(string(d.Matcher))
+ }
+ }
})
}
@@ -657,6 +854,7 @@ func TestMatchByImage(t *testing.T) {
observedMatchers.Remove(string(match.StockMatcher))
definedMatchers.Remove(string(match.StockMatcher))
definedMatchers.Remove(string(match.MsrcMatcher))
+ definedMatchers.Remove(string(match.PortageMatcher)) // TODO: add this back in when #744 is complete
if len(observedMatchers) != len(definedMatchers) {
t.Errorf("matcher coverage incomplete (matchers=%d, coverage=%d)", len(definedMatchers), len(observedMatchers))
@@ -667,14 +865,106 @@ func TestMatchByImage(t *testing.T) {
t.Log(cmp.Diff(defs, obs))
}
+}
+// testIgnoredMatches returns an list of ignored matches to test the vex
+// matchers
+func testIgnoredMatches() []match.IgnoredMatch {
+ return []match.IgnoredMatch{
+ {
+ Match: match.Match{
+ Vulnerability: vulnerability.Vulnerability{
+ Reference: vulnerability.Reference{
+ ID: "CVE-alpine-libvncserver",
+ Namespace: "alpine:distro:alpine:3.12",
+ },
+ },
+ Package: pkg.Package{
+ ID: "44fa3691ae360cac",
+ Name: "libvncserver",
+ Version: "0.9.9",
+ Licenses: []string{"GPL-2.0-or-later"},
+ Type: "apk",
+ CPEs: []cpe.CPE{
+ {
+ Attributes: cpe.Attributes{
+ Part: "a",
+ Vendor: "libvncserver",
+ Product: "libvncserver",
+ Version: "0.9.9",
+ },
+ },
+ },
+ PURL: "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0",
+ Upstreams: []pkg.UpstreamPackage{{Name: "libvncserver"}},
+ },
+ Details: []match.Detail{
+ {
+ Type: "exact-indirect-match",
+ SearchedBy: map[string]any{
+ "distro": map[string]string{
+ "type": "alpine",
+ "version": "3.12.0",
+ },
+ "namespace": "alpine:distro:alpine:3.12",
+ "package": map[string]string{
+ "name": "libvncserver",
+ "version": "0.9.9",
+ },
+ },
+ Found: map[string]any{
+ "versionConstraint": "< 0.9.10 (unknown)",
+ "vulnerabilityID": "CVE-alpine-libvncserver",
+ },
+ Matcher: "apk-matcher",
+ Confidence: 1,
+ },
+ },
+ },
+ AppliedIgnoreRules: []match.IgnoreRule{},
+ },
+ }
+}
+
+// vexMatches moves the first match of a matches list to an ignore list and
+// applies a VEX "affected" document to it to move it to the matches list.
+func vexMatches(t *testing.T, ignoredMatches []match.IgnoredMatch, vexStatus vex.Status, vexDocuments []string) match.Matches {
+ matches := match.NewMatches()
+ vexMatcher := vex.NewProcessor(vex.ProcessorOptions{
+ Documents: vexDocuments,
+ IgnoreRules: []match.IgnoreRule{
+ {VexStatus: string(vexStatus)},
+ },
+ })
+
+ pctx := &pkg.Context{
+ Source: &source.Description{
+ Metadata: source.ImageMetadata{
+ RepoDigests: []string{
+ "alpine@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ },
+ },
+ },
+ }
+
+ vexedMatches, ignoredMatches, err := vexMatcher.ApplyVEX(pctx, &matches, ignoredMatches)
+ if err != nil {
+ t.Errorf("applying VEX data: %s", err)
+ }
+
+ if len(ignoredMatches) != 0 {
+ t.Errorf("VEX text fixture %s must affect all ignored matches (%d left)", vexDocuments, len(ignoredMatches))
+ }
+
+ return *vexedMatches
}
func assertMatches(t *testing.T, expected, actual []match.Match) {
t.Helper()
- var opts = []cmp.Option{
+ opts := []cmp.Option{
+ cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
- cmpopts.IgnoreFields(pkg.Package{}, "Locations"),
+ cmpopts.IgnoreFields(pkg.Package{}, "Locations", "Distro"),
cmpopts.SortSlices(func(a, b match.Match) bool {
return a.Package.ID < b.Package.ID
}),
@@ -684,3 +974,9 @@ func assertMatches(t *testing.T, expected, actual []match.Match) {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
+
+func byNamespace(ns string) vulnerability.Criteria {
+ return search.ByFunc(func(v vulnerability.Vulnerability) (bool, string, error) {
+ return v.Reference.Namespace == ns, "", nil
+ })
+}
diff --git a/test/integration/match_by_sbom_document_test.go b/test/integration/match_by_sbom_document_test.go
index d112f37fd06..c4e766d17f1 100644
--- a/test/integration/match_by_sbom_document_test.go
+++ b/test/integration/match_by_sbom_document_test.go
@@ -11,10 +11,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/grype/grype"
- "github.com/anchore/grype/grype/db"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
- "github.com/anchore/grype/grype/store"
"github.com/anchore/syft/syft/source"
)
@@ -55,18 +53,18 @@ func TestMatchBySBOMDocument(t *testing.T) {
{
name: "unknown package type",
fixture: "test-fixtures/sbom/syft-sbom-with-unknown-packages.json",
- expectedIDs: []string{"CVE-bogus-my-package-2-python"},
+ expectedIDs: []string{"CVE-bogus-my-package-2-idris"},
expectedDetails: []match.Detail{
{
Type: match.ExactDirectMatch,
SearchedBy: map[string]interface{}{
- "language": "python",
- "namespace": "github:language:python",
+ "language": "idris",
+ "namespace": "github:language:idris",
"package": map[string]string{"name": "my-package", "version": "1.0.5"},
},
Found: map[string]interface{}{
- "versionConstraint": "< 2.0 (python)",
- "vulnerabilityID": "CVE-bogus-my-package-2-python",
+ "versionConstraint": "< 2.0 (unknown)",
+ "vulnerabilityID": "CVE-bogus-my-package-2-idris",
},
Matcher: match.StockMatcher,
Confidence: 1,
@@ -77,17 +75,8 @@ func TestMatchBySBOMDocument(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- mkStr := newMockDbStore()
- vp, err := db.NewVulnerabilityProvider(mkStr)
- require.NoError(t, err)
- mp := db.NewVulnerabilityMetadataProvider(mkStr)
- ep := db.NewMatchExclusionProvider(mkStr)
- str := store.Store{
- Provider: vp,
- MetadataProvider: mp,
- ExclusionProvider: ep,
- }
- matches, _, _, err := grype.FindVulnerabilities(str, fmt.Sprintf("sbom:%s", test.fixture), source.SquashedScope, nil)
+ vp := newMockDbProvider()
+ matches, _, _, err := grype.FindVulnerabilities(vp, fmt.Sprintf("sbom:%s", test.fixture), source.SquashedScope, nil)
assert.NoError(t, err)
details := make([]match.Detail, 0)
ids := strset.New()
diff --git a/test/integration/test-fixtures/.gitignore b/test/integration/test-fixtures/.gitignore
index c17a2d0cafb..bc800a849a4 100644
--- a/test/integration/test-fixtures/.gitignore
+++ b/test/integration/test-fixtures/.gitignore
@@ -1,2 +1,3 @@
!**/image-*/Dockerfile
-grype-db
\ No newline at end of file
+grype-db
+grype-db-download*
\ No newline at end of file
diff --git a/test/integration/test-fixtures/image-alpine-match-coverage/Dockerfile b/test/integration/test-fixtures/image-alpine-match-coverage/Dockerfile
index 770e60b5604..2690c3e0e32 100644
--- a/test/integration/test-fixtures/image-alpine-match-coverage/Dockerfile
+++ b/test/integration/test-fixtures/image-alpine-match-coverage/Dockerfile
@@ -1,2 +1,7 @@
+FROM cgr.dev/chainguard/go as builder
+
+RUN go install github.com/google/ko@v0.15.1
+
FROM scratch
-COPY . .
\ No newline at end of file
+COPY . .
+COPY --from=builder /root/go/bin/ko /ko
diff --git a/test/integration/test-fixtures/image-alpine-match-coverage/lib/apk/db/installed b/test/integration/test-fixtures/image-alpine-match-coverage/lib/apk/db/installed
index 0afacfe326c..2e06725161c 100644
--- a/test/integration/test-fixtures/image-alpine-match-coverage/lib/apk/db/installed
+++ b/test/integration/test-fixtures/image-alpine-match-coverage/lib/apk/db/installed
@@ -27,3 +27,32 @@ Z:Q184HrHsxEBqnsH4QNxeU5w8alhKI=
R:libvncclient.so.1.0.0
a:0:0:755
Z:Q1IEjCrEwVlQt2GjIsb3o39vcgqMg=
+
+C:Q1z0MwWQKfva+S+q7XmOBYFfQgW/k=
+P:ko
+V:0.15.1
+A:x86_64
+S:166239
+I:389120
+T:Build and deploy Go applications
+o:ko
+t:1572818861
+R:ko
+a:0:0:755
+Z:Q16Pd1AqyqQRMwiFfbUt9XkYnkapw=
+
+C:Q1z0MwWQKfva+S+q7XmOBYFfQgW/k=
+P:npm-apk-subpackage-with-false-positive
+V:7.0.0
+A:x86_64
+S:166239
+I:389120
+T:NPM package, in an APK subpackage, that has a false positive
+o:npm-apk-package-with-false-positive
+t:1572818861
+F:lib
+F:lib/node_modules
+F:lib/node_modules/npm-apk-subpackage-with-false-positive
+R:package.json
+a:0:0:755
+Z:Q16Pd1AqyqQRMwiFfbUt9XkYnkapw=
diff --git a/test/integration/test-fixtures/image-alpine-match-coverage/lib/node_modules/npm-apk-subpackage-with-false-positive/package.json b/test/integration/test-fixtures/image-alpine-match-coverage/lib/node_modules/npm-apk-subpackage-with-false-positive/package.json
new file mode 100644
index 00000000000..4684622b6a5
--- /dev/null
+++ b/test/integration/test-fixtures/image-alpine-match-coverage/lib/node_modules/npm-apk-subpackage-with-false-positive/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "npm-apk-subpackage-with-false-positive",
+ "version": "1.0.0",
+ "description": "NPM package, in an APK subpackage, that has a false positive",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/luhring/grype.git"
+ }
+}
diff --git a/test/integration/test-fixtures/image-jvm-match-coverage/Dockerfile b/test/integration/test-fixtures/image-jvm-match-coverage/Dockerfile
new file mode 100644
index 00000000000..04fdc270146
--- /dev/null
+++ b/test/integration/test-fixtures/image-jvm-match-coverage/Dockerfile
@@ -0,0 +1,2 @@
+FROM scratch
+COPY . .
diff --git a/test/integration/test-fixtures/image-jvm-match-coverage/opt/java/openjdk/release b/test/integration/test-fixtures/image-jvm-match-coverage/opt/java/openjdk/release
new file mode 100644
index 00000000000..532486dc639
--- /dev/null
+++ b/test/integration/test-fixtures/image-jvm-match-coverage/opt/java/openjdk/release
@@ -0,0 +1,5 @@
+JAVA_VERSION="1.8.0_400"
+FULL_VERSION="1.8.0_400-b07"
+NOPE_SEMANTIC_VERSION="8.0.400+7"
+IMPLEMENTOR="Oracle"
+IMAGE_TYPE="JDK"
diff --git a/test/integration/test-fixtures/image-rust-auditable-match-coverage/Dockerfile b/test/integration/test-fixtures/image-rust-auditable-match-coverage/Dockerfile
new file mode 100644
index 00000000000..ac01990fce0
--- /dev/null
+++ b/test/integration/test-fixtures/image-rust-auditable-match-coverage/Dockerfile
@@ -0,0 +1,2 @@
+# An image containing the example hello-auditable binary from https://github.com/Shnatsel/rust-audit/tree/master/hello-auditable
+FROM docker.io/tofay/hello-rust-auditable:latest
\ No newline at end of file
diff --git a/test/integration/test-fixtures/sbom/syft-sbom-with-unknown-packages.json b/test/integration/test-fixtures/sbom/syft-sbom-with-unknown-packages.json
index 29781074a39..bd194840d14 100644
--- a/test/integration/test-fixtures/sbom/syft-sbom-with-unknown-packages.json
+++ b/test/integration/test-fixtures/sbom/syft-sbom-with-unknown-packages.json
@@ -4,8 +4,8 @@
"id": "eeb36c1c-c03a-425b-901f-df918cc3757e",
"name": "my-package",
"version": "1.0.5",
- "type": "binary",
- "language": "python",
+ "type": "idris",
+ "language": "idris",
"cpes": [
"cpe:2.3:a:my-package:my-package:1.0.5:*:*:*:*:*:*:*",
"cpe:2.3:a:bogus:my-package:1.0.5:*:*:*:*:*:*:*"
diff --git a/test/integration/test-fixtures/vex/openvex/affected.openvex.json b/test/integration/test-fixtures/vex/openvex/affected.openvex.json
new file mode 100644
index 00000000000..237ebb9de3e
--- /dev/null
+++ b/test/integration/test-fixtures/vex/openvex/affected.openvex.json
@@ -0,0 +1,23 @@
+{
+ "@context": "https://openvex.dev/ns/v0.2.0",
+ "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
+ "author": "The OpenVEX Project ",
+ "timestamp": "2023-07-17T18:28:47.696004345-06:00",
+ "version": 1,
+ "statements": [
+ {
+ "vulnerability": {
+ "name": "CVE-alpine-libvncserver"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/alpine@sha256%3Affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "subcomponents": [
+ { "@id": "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0" }
+ ]
+ }
+ ],
+ "status": "affected"
+ }
+ ]
+}
diff --git a/test/integration/test-fixtures/vex/openvex/under_investigation.openvex.json b/test/integration/test-fixtures/vex/openvex/under_investigation.openvex.json
new file mode 100644
index 00000000000..f9e4c60e38e
--- /dev/null
+++ b/test/integration/test-fixtures/vex/openvex/under_investigation.openvex.json
@@ -0,0 +1,24 @@
+{
+ "@context": "https://openvex.dev/ns/v0.2.0",
+ "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
+ "author": "The OpenVEX Project ",
+ "timestamp": "2023-07-17T18:28:47.696004345-06:00",
+ "version": 1,
+ "statements": [
+ {
+ "timestamp": "2023-07-16T18:28:47.696004345-06:00",
+ "vulnerability": {
+ "name": "CVE-alpine-libvncserver"
+ },
+ "products": [
+ {
+ "@id": "pkg:oci/alpine@sha256%3Affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "subcomponents": [
+ { "@id": "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0" }
+ ]
+ }
+ ],
+ "status": "under_investigation"
+ }
+ ]
+}
diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go
index c86ae5c6264..eb203ae62ce 100644
--- a/test/integration/utils_test.go
+++ b/test/integration/utils_test.go
@@ -1,6 +1,8 @@
package integration
import (
+ "bytes"
+ "context"
"errors"
"fmt"
"os"
@@ -15,7 +17,6 @@ import (
"github.com/anchore/grype/grype/match"
"github.com/anchore/syft/syft"
- "github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
@@ -48,7 +49,7 @@ func PullThroughImageCache(t testing.TB, imageName string) string {
func saveImage(t testing.TB, imageName string, destPath string) {
sourceImage := fmt.Sprintf("docker://docker.io/%s", imageName)
destinationString := fmt.Sprintf("docker-archive:%s", destPath)
- skopeoPath := filepath.Join(repoRoot(t), ".tmp", "skopeo")
+ skopeoPath := filepath.Join(repoRoot(t), ".tool", "skopeo")
policyPath := filepath.Join(repoRoot(t), "test", "integration", "test-fixtures", "skopeo-policy.json")
skopeoCommand := []string{
@@ -70,39 +71,28 @@ func saveImage(t testing.TB, imageName string, destPath string) {
t.Logf("Stdout: %s\n", out)
}
-func getSyftSBOM(t testing.TB, image string, format sbom.Format) string {
- detection, err := source.Detect(image, source.DetectConfig{})
- if err != nil {
- t.Fatalf("could not generate source input for packages command: %+v", err)
- }
+func getSyftSBOM(t testing.TB, image, from string, encoder sbom.FormatEncoder) string {
+ src, err := syft.GetSource(context.Background(), image, syft.DefaultGetSourceConfig().WithSources(from))
+ require.NoError(t, err)
- src, err := detection.NewSource(source.DetectionSourceConfig{})
- if err != nil {
- t.Fatalf("can't get the source: %+v", err)
- }
t.Cleanup(func() {
require.NoError(t, src.Close())
})
- config := cataloger.DefaultConfig()
+ config := syft.DefaultCreateSBOMConfig()
+
config.Search.Scope = source.SquashedScope
// TODO: relationships are not verified at this time
- collection, _, distro, err := syft.CatalogPackages(src, config)
-
- s := sbom.SBOM{
- Artifacts: sbom.Artifacts{
- Packages: collection,
- LinuxDistribution: distro,
- },
- Source: src.Describe(),
- }
+ s, err := syft.CreateSBOM(context.Background(), src, config)
+ require.NoError(t, err)
+ require.NotNil(t, s)
- bytes, err := syft.Encode(s, format)
- if err != nil {
- t.Fatalf("presenter failed: %+v", err)
- }
+ var buf bytes.Buffer
+
+ err = encoder.Encode(&buf, *s)
+ require.NoError(t, err)
- return string(bytes)
+ return buf.String()
}
func getMatchSet(matches match.Matches) *strset.Set {
diff --git a/test/quality/.gitignore b/test/quality/.gitignore
index 92d889aa0b7..3e788c6aa4d 100644
--- a/test/quality/.gitignore
+++ b/test/quality/.gitignore
@@ -4,4 +4,6 @@ venv
stage
pull
migrate.py
-.oras-cache
\ No newline at end of file
+.oras-cache
+*.tar.gz
+*.tar.zst
\ No newline at end of file
diff --git a/test/quality/.grype.yaml b/test/quality/.grype.yaml
new file mode 100644
index 00000000000..bdb454059e8
--- /dev/null
+++ b/test/quality/.grype.yaml
@@ -0,0 +1,25 @@
+by-cve: true
+
+# we want to be able to validate CPE findings with the broadest lens possible (not just the default configuration)
+# to aid in validating CPE related changes (whereas the default configuration is more focused on non CPE matching)
+match:
+ java:
+ using-cpes: true
+ jvm:
+ using-cpes: true
+ dotnet:
+ using-cpes: true
+ golang:
+ using-cpes: true
+ always-use-cpe-for-stdlib: true
+ javascript:
+ using-cpes: true
+ python:
+ using-cpes: true
+ ruby:
+ using-cpes: true
+ rust:
+ using-cpes: true
+ stock:
+ using-cpes: true
+
diff --git a/test/quality/.yardstick.yaml b/test/quality/.yardstick.yaml
index e580fec4b9d..3f7c8c916d3 100644
--- a/test/quality/.yardstick.yaml
+++ b/test/quality/.yardstick.yaml
@@ -4,6 +4,7 @@ x-ref:
- docker.io/cloudbees/cloudbees-core-agent:2.289.2.2@sha256:d48f0546b4cf5ef4626136242ce302f94a42751156b7be42f4b1b75a66608880
- docker.io/cloudbees/cloudbees-core-mm:2.277.3.1@sha256:4c564f473d38f23da1caa48c4ef53b958ef03d279232007ad3319b1f38584bdb
- docker.io/cloudbees/cloudbees-core-oc:2.289.2.2@sha256:9cd85ee84e401dc27e3a8268aae67b594a651b2f4c7fc056ca14c7b0a0a6b82d
+ - docker.io/anchore/test_images:grype-quality-dotnet-69f15d2@sha256:e25a9a175433c2bfe9c04e6482e6c5eca0491629144c78061763f7f604fdea80
- docker.io/anchore/test_images:grype-quality-node-d89207b@sha256:f56164678054e5eb59ab838367373a49df723b324617b1ba6de775749d7f91d4
- docker.io/anchore/test_images:grype-quality-python-d89207b@sha256:b2b58a55c0b03c1626d2aaae2add9832208b02124dda7b7b41811e14f0fb272c
- docker.io/anchore/test_images:grype-quality-java-d89207b@sha256:b3534fc2e37943136d5b54e3a58b55d4ccd4363d926cf7aa5bf55a524cf8275b
@@ -13,6 +14,8 @@ x-ref:
- docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9
- docker.io/anchore/test_images:appstreams-oraclelinux-8-1a287dd@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1
- docker.io/anchore/test_images:appstreams-rhel-8-1a287dd@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b
+ - docker.io/anchore/test_images:appstreams-nodejs-base-rhel-9-1b0b1b4@sha256:fc6f7a37d7e320f6ff3643d4ec9a208adb1462cd16027f045b56563e12bb0461
+ - docker.io/anchore/test_images:appstreams-nodejs-18-rhel-9-1b0b1b4@sha256:08dbfad2d6af9afe47f7647b0b8f38fd29fc9e89306cfc39c9509981f9388b7f
- docker.io/anchore/test_images:java-56d52bc@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da
- docker.io/anchore/test_images:npm-56d52bc@sha256:ba42ded8613fc643d407a050faf5ab48cfb405ad3ef2015bf6feeb5dff44738d
- docker.io/anchore/test_images:gems-56d52bc@sha256:5763c8a225f950961bf01ddec68e36f18e236130e182f2b9290a6e03b9777bfe
@@ -41,7 +44,8 @@ x-ref:
- registry.access.redhat.com/ubi8@sha256:68fecea0d255ee253acbf0c860eaebb7017ef5ef007c25bee9eeffd29ce85b29
- docker.io/python:3.8.0-slim@sha256:5e96e03a493a54904aa8be573fc0414431afb4f47ac58fbffd03b2a725005364
- docker.io/ghost:5.2.4@sha256:42137b9bd1faf4cdea5933279c48a912d010ef614551aeb0e44308600aa3e69f
- - docker.io/node:4.2.1-slim@sha256:af31633b87d0dc58c306b04ad9f6ca88104626363c5c085e9962832628eb09ce
+# commented out lines in this list are Docker v1 images, which no longer work after docker daemon dropped support
+# - #docker.io/node:4.2.1-slim@sha256:af31633b87d0dc58c306b04ad9f6ca88104626363c5c085e9962832628eb09ce
- docker.io/elastic/kibana:8.5.0@sha256:b9e3e52f61e0a347e38eabe80ba0859f859023bc0cc8836410320aa7eb5d3e02
- docker.io/jenkins/jenkins:2.361.4-lts-jdk11@sha256:6fd5699ab182b5d23d0e3936de6047edc30955a3a92e01c392d5a2fd583efac0
- docker.io/neo4j:4.4.14-community@sha256:fcfcbb026e0e538bf66f5fe5c4b2db3dd4931c3aae07f13a5a8c10e979596256
@@ -57,11 +61,11 @@ x-ref:
- docker.io/grafana/grafana:9.2.4@sha256:a11c6829cdfe7fd791e48ba5b511f3562384361fb4c568ec2d8a5041ac52babe
- docker.io/hashicorp/vault:1.12.0@sha256:09354ca0891f7cee8fbfe8db08c62d2d757fad8ae6c91f2b6cce7a34440e3fae
- docker.io/ubuntu:12.04@sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005
- - docker.io/ubuntu:12.10@sha256:002fba3e3255af10be97ea26e476692a7ebed0bb074a9ab960b2e7a1526b15d7
- - docker.io/ubuntu:13.04@sha256:bc48dd7075ce920ebbaa4581d3200e9fb3aaec31591061d7e3a280a04ef0248c
+# - #docker.io/ubuntu:12.10@sha256:002fba3e3255af10be97ea26e476692a7ebed0bb074a9ab960b2e7a1526b15d7
+# - #docker.io/ubuntu:13.04@sha256:bc48dd7075ce920ebbaa4581d3200e9fb3aaec31591061d7e3a280a04ef0248c
- docker.io/ubuntu:14.04@sha256:881afbae521c910f764f7187dbfbca3cc10c26f8bafa458c76dda009a901c29d
- - docker.io/ubuntu:14.10@sha256:6341c688b4b0b82ec735389b3c97df8cf2831b8cb8bd1856779130a86574ac5c
- - docker.io/ubuntu:15.04@sha256:2fb27e433b3ecccea2a14e794875b086711f5d49953ef173d8a03e8707f1510f
+# - #docker.io/ubuntu:14.10@sha256:6341c688b4b0b82ec735389b3c97df8cf2831b8cb8bd1856779130a86574ac5c
+# - #docker.io/ubuntu:15.04@sha256:2fb27e433b3ecccea2a14e794875b086711f5d49953ef173d8a03e8707f1510f
- docker.io/ubuntu:15.10@sha256:02521a2d079595241c6793b2044f02eecf294034f31d6e235ac4b2b54ffc41f3
- docker.io/ubuntu:16.10@sha256:8dc9652808dc091400d7d5983949043a9f9c7132b15c14814275d25f94bca18a
- docker.io/ubuntu:17.04@sha256:213e05583a7cb8756a3f998e6dd65204ddb6b4c128e2175dcdf174cdf1877459
@@ -78,6 +82,8 @@ x-ref:
- docker.io/ubuntu:22.10@sha256:80fb4ea0c0a384a3072a6be1879c342bb636b0d105209535ba893ba75ab38ede
- docker.io/ubuntu:23.04@sha256:09f035f46361d193ded647342903b413d57d05cc06acff8285f9dda9f2d269d5
- gcr.io/distroless/python3-debian11@sha256:69ae7f133d33faab720af28e78fb45707b623bcbc94ae02a07c633bf053f4b40
+ - registry.suse.com/suse/sles12sp4:26.380@sha256:94b537f5b312e7397b5d0bbb3d892f961acdd9454950fc233d77f771e25335fb
+ - registry.suse.com/suse/sle15:15.1.6.2.461@sha256:6e613c994c3b33224e439ef8ee9003fb69416f77f7a6b1da0b18981d5aa3bb75
# new vulnerabilities are added all of the time, instead of keeping up it's easier to ignore newer entries.
# This approach helps tremendously with keeping the analysis relatively stable.
@@ -86,6 +92,11 @@ default_max_year: 2021
result-sets:
pr_vs_latest_via_sbom:
description: "latest released grype vs grype from the current build (via SBOM ingestion)"
+ validations:
+ - max-f1-regression: 0.0
+ max-new-false-negatives: 00
+ max-unlabeled-percent: 10
+ max_year: 2021
matrix:
images: *images
@@ -93,14 +104,69 @@ result-sets:
- name: syft
# note: we want to use a fixed version of syft for capturing all results (NOT "latest")
- version: v0.74.1
+ version: v1.14.0
produces: SBOM
refresh: false
- name: grype
- version: git:current-commit
+ # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The
+ # point of this test is to ensure the correctness of the logic in grype itself with real production data.
+ # By pinning the DB the grype code itself becomes the independent variable under test (and not the
+ # every-changing DB). That being said, we should be updating this DB periodically to ensure what we
+ # are testing with is not too stale.
+ # version: git:current-commit+import-db=db.tar.zst
+ # for local build of grype, use for example:
+ version: path:../../+import-db=db.tar.zst
takes: SBOM
+ label: candidate
- name: grype
- version: latest
+ # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The
+ # point of this test is to ensure the correctness of the logic in grype itself with real production data.
+ # By pinning the DB the grype code itself becomes the independent variable under test (and not the
+ # every-changing DB). That being said, we should be updating this DB periodically to ensure what we
+ # are testing with is not too stale.
+ version: latest+import-db=db.tar.zst
takes: SBOM
+ label: reference
+ pr_vs_latest_via_sbom_2022:
+ description: "same as 'pr_vs_latest_via_sbom', but includes vulnerabilities from 2022 and before, instead of 2021 and before"
+ max_year: 2022
+ validations:
+ - max-f1-regression: 0.1 # allowed to regress 0.1 on f1 score
+ max-new-false-negatives: 10
+ max-unlabeled-percent: 0
+ max_year: 2022
+ fail_on_empty_match_set: false
+ matrix:
+ images:
+ - docker.io/anchore/test_images:azurelinux3-63671fe@sha256:2d761ba36575ddd4e07d446f4f2a05448298c20e5bdcd3dedfbbc00f9865240d
+
+ tools:
+ - name: syft
+ # note: we want to use a fixed version of syft for capturing all results (NOT "latest")
+ version: v1.14.0
+ produces: SBOM
+ refresh: false
+
+ - name: grype
+ # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The
+ # point of this test is to ensure the correctness of the logic in grype itself with real production data.
+ # By pinning the DB the grype code itself becomes the independent variable under test (and not the
+ # every-changing DB). That being said, we should be updating this DB periodically to ensure what we
+ # are testing with is not too stale.
+ # version: git:current-commit+import-db=db.tar.zst
+ # for local build of grype, use for example:
+ version: path:../../+import-db=db.tar.zst
+ takes: SBOM
+ label: candidate # is candidate better than the current baseline?
+
+ - name: grype
+ # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The
+ # point of this test is to ensure the correctness of the logic in grype itself with real production data.
+ # By pinning the DB the grype code itself becomes the independent variable under test (and not the
+ # every-changing DB). That being said, we should be updating this DB periodically to ensure what we
+ # are testing with is not too stale.
+ version: latest+import-db=db.tar.zst
+ takes: SBOM
+ label: reference # this run is the current baseline
diff --git a/test/quality/Makefile b/test/quality/Makefile
index 65e1c96c859..3ba091a1175 100644
--- a/test/quality/Makefile
+++ b/test/quality/Makefile
@@ -7,6 +7,11 @@ YARDSTICK_LABELS_DIR = .yardstick/labels
VULNERABILITY_LABELS = ./vulnerability-labels
RESULT_SET = pr_vs_latest_via_sbom
+# update periodically with values from "grype db list"
+TEST_DB_URL_FILE = ./test-db
+TEST_DB_URL = "https://grype.anchore.io/databases/v6/$(shell cat $(TEST_DB_URL_FILE))"
+TEST_DB = db.tar.zst
+
# formatting variables
BOLD := $(shell tput -T linux bold)
PURPLE := $(shell tput -T linux setaf 5)
@@ -21,24 +26,28 @@ SUCCESS := $(BOLD)$(GREEN)
all: capture validate ## Fetch or capture all data and run all quality checks
.PHONY: validate
-validate: venv $(VULNERABILITY_LABELS) ## Run all quality checks against already collected data
- $(ACTIVATE_VENV) ./gate.py
+validate: venv $(VULNERABILITY_LABELS)/Makefile ## Run all quality checks against already collected data
+ $(YARDSTICK) validate -r $(RESULT_SET) -r $(RESULT_SET)_2022
.PHONY: capture
capture: sboms vulns ## Collect and store all syft and grype results
-.PHONY: capture
-vulns: venv ## Collect and store all grype results
+.PHONY: vulns
+vulns: venv $(TEST_DB) ## Collect and store all grype results
$(YARDSTICK) -v result capture -r $(RESULT_SET)
+ $(YARDSTICK) -v result capture -r $(RESULT_SET)_2022
+
+$(TEST_DB):
+ @curl -o $(TEST_DB) -SsL $(TEST_DB_URL)
.PHONY: sboms
sboms: $(YARDSTICK_RESULT_DIR) venv clear-results ## Collect and store all syft results (deletes all existing results)
- bash -c "make download-sboms || ($(YARDSTICK) -v result capture -r $(RESULT_SET) --only-producers)"
+ bash -c "make download-sboms || ($(YARDSTICK) -v result capture -r $(RESULT_SET) --only-producers && $(YARDSTICK) -v result capture -r $(RESULT_SET)_2022 --only-producers)"
.PHONY: download-sboms
-download-sboms:
+download-sboms: $(VULNERABILITY_LABELS)/Makefile
cd vulnerability-match-labels && make venv
- bash -c "export ORAS_CACHE=$(shell pwd)/.oras-cache && make venv && . vulnerability-match-labels/venv/bin/activate && ./vulnerability-match-labels/sboms.py download -r $(RESULT_SET)"
+ bash -c "export ORAS_CACHE=$(shell pwd)/.oras-cache && make venv && . vulnerability-match-labels/venv/bin/activate && ./vulnerability-match-labels/sboms.py download -r $(RESULT_SET) && ./vulnerability-match-labels/sboms.py download -r $(RESULT_SET)_2022"
venv: venv/touchfile
@@ -47,11 +56,10 @@ venv/touchfile: requirements.txt
$(ACTIVATE_VENV) pip install -Ur requirements.txt
touch venv/touchfile
-
$(YARDSTICK_RESULT_DIR):
mkdir -p $(YARDSTICK_RESULT_DIR)
-$(VULNERABILITY_LABELS):
+$(VULNERABILITY_LABELS)/Makefile:
git submodule update vulnerability-match-labels
.PHONY: clear-results
diff --git a/test/quality/gate.py b/test/quality/gate.py
deleted file mode 100755
index 3d615f98d16..00000000000
--- a/test/quality/gate.py
+++ /dev/null
@@ -1,326 +0,0 @@
-#!/usr/bin/env python3
-import logging
-import os
-import re
-import subprocess
-import sys
-from typing import Optional
-
-import click
-from tabulate import tabulate
-from dataclasses import dataclass, InitVar, field
-
-import yardstick
-from yardstick import store, comparison, artifact, arrange
-from yardstick.cli import display, config
-
-
-# see the .yardstick.yaml configuration for details
-default_result_set = "pr_vs_latest_via_sbom"
-yardstick.utils.grype_db.raise_on_failure(False)
-
-@dataclass
-class Gate:
- label_comparisons: InitVar[Optional[list[comparison.AgainstLabels]]]
- label_comparison_stats: InitVar[Optional[comparison.ImageToolLabelStats]]
-
- reasons: list[str] = field(default_factory=list)
-
- def __post_init__(self, label_comparisons: Optional[list[comparison.AgainstLabels]], label_comparison_stats: Optional[comparison.ImageToolLabelStats]):
- if not label_comparisons and not label_comparison_stats:
- return
-
- reasons = []
-
- # - fail when current F1 score drops below last release F1 score (or F1 score is indeterminate)
- # - fail when indeterminate % > 10%
- # - fail when there is a rise in FNs
- latest_release_tool, current_tool = guess_tool_orientation(label_comparison_stats.tools)
-
- latest_release_comparisons_by_image = {comp.config.image: comp for comp in label_comparisons if comp.config.tool == latest_release_tool }
- current_comparisons_by_image = {comp.config.image: comp for comp in label_comparisons if comp.config.tool == current_tool }
-
- for image, comp in current_comparisons_by_image.items():
- latest_f1_score = latest_release_comparisons_by_image[image].summary.f1_score
- current_f1_score = comp.summary.f1_score
- if current_f1_score < latest_f1_score:
- reasons.append(f"current F1 score is lower than the latest release F1 score: {bcolors.BOLD+bcolors.UNDERLINE}current={current_f1_score:0.2f} latest={latest_f1_score:0.2f}{bcolors.RESET} image={image}")
-
- if comp.summary.indeterminate_percent > 10:
- reasons.append(f"current indeterminate matches % is greater than 10%: {bcolors.BOLD+bcolors.UNDERLINE}current={comp.summary.indeterminate_percent:0.2f}%{bcolors.RESET} image={image}")
-
- latest_fns = latest_release_comparisons_by_image[image].summary.false_negatives
- current_fns = comp.summary.false_negatives
- if current_fns > latest_fns:
- reasons.append(f"current false negatives is greater than the latest release false negatives: {bcolors.BOLD+bcolors.UNDERLINE}current={current_fns} latest={latest_fns}{bcolors.RESET} image={image}")
-
- self.reasons = reasons
-
- def passed(self):
- return len(self.reasons) == 0
-
-def guess_tool_orientation(tools: list[str]):
- if len(tools) != 2:
- raise RuntimeError("expected 2 tools, got %s" % tools)
-
- current_tool = None
- latest_release_tool = None
- for tool in tools:
- if tool.endswith("latest"):
- latest_release_tool = tool
- continue
- current_tool = tool
-
- if latest_release_tool is None:
- # "latest" value isn't accessible, so we do a best guess at which version is latest
- latest_release_tool, current_tool = sorted(tools)
-
- if current_tool is None:
- raise ValueError("current tool not found")
- return latest_release_tool, current_tool
-
-class bcolors:
- HEADER = '\033[95m'
- OKBLUE = '\033[94m'
- OKCYAN = '\033[96m'
- OKGREEN = '\033[92m'
- WARNING = '\033[93m'
- FAIL = '\033[91m'
- BOLD = '\033[1m'
- UNDERLINE = '\033[4m'
- RESET = '\033[0m'
-
-def show_results_used(results: list[artifact.ScanResult]):
- print(f" Results used:")
- for idx, result in enumerate(results):
- branch = "├──"
- if idx == len(results) - 1:
- branch = "└──"
- print(f" {branch} {result.ID} : {result.config.tool} against {result.config.image}")
- print()
-
-def validate(cfg: config.Application, result_set: str, images: list[str], always_run_label_comparison: bool, verbosity: int, label_entries: Optional[list[artifact.LabelEntry]] = None):
- print(f"{bcolors.HEADER}{bcolors.BOLD}Validating with {result_set!r}", bcolors.RESET)
- result_set_obj = store.result_set.load(name=result_set)
-
- ret = []
- for image, result_states in result_set_obj.result_state_by_image.items():
- if images and image not in images:
- print("Skipping image:", image)
- continue
- print()
- print("Testing image:", image)
- for state in result_states:
- print(" ", f"with {state.request.tool}")
- print()
-
- gate = validate_image(cfg, [s.config.path for s in result_states], always_run_label_comparison=always_run_label_comparison, verbosity=verbosity, label_entries=label_entries)
- ret.append(gate)
-
- failure = not gate.passed()
- if failure:
- print(f"{bcolors.FAIL}{bcolors.BOLD}Failed quality gate{bcolors.RESET}")
- for reason in gate.reasons:
- print(f" - {reason}")
-
- print()
- size = 120
- print("▁"*size)
- print("░"*size)
- print("▔"*size)
- return ret
-
-def validate_image(cfg: config.Application, descriptions: list[str], always_run_label_comparison: bool, verbosity: int, label_entries: Optional[list[artifact.LabelEntry]] = None):
- # do a relative comparison
- # - show comparison summary (no gating action)
- # - list out all individual match differences
-
- print(f"{bcolors.HEADER}Running relative comparison...", bcolors.RESET)
- relative_comparison = yardstick.compare_results(descriptions=descriptions, year_max_limit=cfg.default_max_year)
- show_results_used(relative_comparison.results)
-
- # show the relative comparison results
- if verbosity > 0:
- details = verbosity > 1
- display.preserved_matches(relative_comparison, details=details, summary=True, common=False)
- print()
-
- # bail if there are no differences found
- if not always_run_label_comparison and not sum([len(relative_comparison.unique[result.ID]) for result in relative_comparison.results]):
- print("no differences found between tool results")
- return Gate(None, None)
-
- # do a label comparison
- print(f"{bcolors.HEADER}Running comparison against labels...", bcolors.RESET)
- results, label_entries, comparisons_by_result_id, stats_by_image_tool_pair = yardstick.compare_results_against_labels(descriptions=descriptions, year_max_limit=cfg.default_max_year, label_entries=label_entries)
- show_results_used(results)
-
- if verbosity > 0:
- show_fns = verbosity > 1
- display.label_comparison(
- results,
- comparisons_by_result_id,
- stats_by_image_tool_pair,
- show_fns=show_fns,
- show_summaries=True,
- )
-
- latest_release_tool, current_tool = guess_tool_orientation([r.config.tool for r in results])
-
- # show the relative comparison unique differences paired up with label conclusions (TP/FP/FN/TN/Unknown)
- all_rows: list[list[Any]] = []
- for result in relative_comparison.results:
- label_comparison = comparisons_by_result_id[result.ID]
- for unique_match in relative_comparison.unique[result.ID]:
- labels = label_comparison.labels_by_match[unique_match.ID]
- if not labels:
- label = "(unknown)"
- elif len(set(labels)) > 1:
- label = ", ".join([l.name for l in labels])
- else:
- label = labels[0].name
-
-
- color = ""
- commentary = ""
- if result.config.tool == latest_release_tool:
- # the tool which found the unique result is the latest release tool...
- if label == artifact.Label.TruePositive.name:
- # drats! we missed a case (this is a new FN)
- color = bcolors.FAIL
- commentary = "(this is a new FN 😱)"
- elif artifact.Label.FalsePositive.name in label:
- # we got rid of a FP! ["hip!", "hip!"]
- color = bcolors.OKBLUE
- commentary = "(got rid of a former FP 🙌)"
- else:
- # the tool which found the unique result is the current tool...
- if label == artifact.Label.TruePositive.name:
- # highest of fives! we found a new TP that the previous tool release missed!
- color = bcolors.OKBLUE
- commentary = "(this is a new TP 🙌)"
- elif artifact.Label.FalsePositive.name in label:
- # welp, our changes resulted in a new FP... not great, maybe not terrible?
- color = bcolors.FAIL
- commentary = "(this is a new FP 😱)"
-
- all_rows.append(
- [
- f"{color}{result.config.tool} ONLY{bcolors.RESET}",
- f"{color}{unique_match.package.name}@{unique_match.package.version}{bcolors.RESET}",
- f"{color}{unique_match.vulnerability.id}{bcolors.RESET}",
- f"{color}{label}{bcolors.RESET}",
- f"{commentary}",
- ]
- )
-
- def escape_ansi(line):
- ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
- return ansi_escape.sub('', line)
-
- # sort but don't consider ansi escape codes
- all_rows = sorted(all_rows, key=lambda x: escape_ansi(str(x[0]+x[1]+x[2]+x[3])))
- if len(all_rows) == 0:
- print("No differences found between tooling (with labels)")
- else:
- print("Match differences between tooling (with labels):")
- indent = " "
- print(indent + tabulate([["TOOL PARTITION", "PACKAGE", "VULNERABILITY", "LABEL", "COMMENTARY"]]+all_rows, tablefmt="plain").replace("\n", "\n" + indent) + "\n")
-
-
- # populate the quality gate with data that can evaluate pass/fail conditions
- return Gate(label_comparisons=comparisons_by_result_id.values(), label_comparison_stats=stats_by_image_tool_pair)
-
-@click.command()
-@click.option("--image", "-i", "images", multiple=True, help="filter down to one or more images to validate with (don't use the full result set)")
-@click.option("--label-comparison", "-l", "always_run_label_comparison", is_flag=True, help="run label comparison irregardless of relative comparison results")
-@click.option("--breakdown-by-ecosystem", "-e", is_flag=True, help="show label comparison results broken down by ecosystem")
-@click.option("--verbose", "-v", "verbosity", count=True, help="show details of all comparisons")
-@click.option("--result-set", "-r", default=default_result_set, help="the result set to use for the quality gate")
-def main(images: list[str], always_run_label_comparison: bool, breakdown_by_ecosystem: bool, verbosity: int, result_set: str):
- cfg = config.load()
- setup_logging(verbosity)
-
- # let's not load any more labels than we need to, base this off of the images we're validating
- if not images:
- images = set()
- result_set_obj = store.result_set.load(name=result_set)
- for state in result_set_obj.state:
- images.add(state.config.image)
- images = sorted(list(images))
-
- print("Loading label entries...", end=" ")
- label_entries = store.labels.load_for_image(images, year_max_limit=cfg.default_max_year)
- print(f"done! {len(label_entries)} entries loaded")
-
- result_sets = [result_set] # today only one result set is supported, but more can be added
- gates = []
- for result_set in result_sets:
- gates.extend(validate(cfg, result_set, images=images, always_run_label_comparison=always_run_label_comparison, verbosity=verbosity, label_entries=label_entries))
- print()
-
- if breakdown_by_ecosystem:
- print(f"{bcolors.HEADER}Breaking down label comparison by ecosystem performance...", bcolors.RESET)
- results_by_image, label_entries, stats = yardstick.compare_results_against_labels_by_ecosystem(result_set=result_set, year_max_limit=cfg.default_max_year, label_entries=label_entries)
- display.labels_by_ecosystem_comparison(
- results_by_image,
- stats,
- show_images_used=False,
- )
- print()
-
- failure = not all([gate.passed() for gate in gates])
- if failure:
- print("Reasons for quality gate failure:")
- for gate in gates:
- for reason in gate.reasons:
- print(f" - {reason}")
-
- if failure:
- print()
- print(f"{bcolors.FAIL}{bcolors.BOLD}Quality gate FAILED{bcolors.RESET}")
- sys.exit(1)
- else:
- print(f"{bcolors.OKGREEN}{bcolors.BOLD}Quality gate passed!{bcolors.RESET}")
-
-
-def setup_logging(verbosity: int):
- # pylint: disable=redefined-outer-name, import-outside-toplevel
- import logging.config
-
- if verbosity in [0, 1, 2]:
- log_level = "WARN"
- elif verbosity == 3:
- log_level = "INFO"
- else:
- log_level = "DEBUG"
-
- logging.config.dictConfig(
- {
- "version": 1,
- "formatters": {
- "standard": {
- # [%(module)s.%(funcName)s]
- "format": "%(asctime)s [%(levelname)s] %(message)s",
- "datefmt": "",
- },
- },
- "handlers": {
- "default": {
- "level": log_level,
- "formatter": "standard",
- "class": "logging.StreamHandler",
- "stream": "ext://sys.stderr",
- },
- },
- "loggers": {
- "": { # root logger
- "handlers": ["default"],
- "level": log_level,
- },
- },
- }
- )
-
-if __name__ == '__main__':
- main()
\ No newline at end of file
diff --git a/test/quality/requirements.txt b/test/quality/requirements.txt
index d38d58a1710..9fc86ce9139 100644
--- a/test/quality/requirements.txt
+++ b/test/quality/requirements.txt
@@ -1,3 +1,3 @@
-git+https://github.com/anchore/yardstick@v0.6.0
+yardstick==v0.12.2
# ../../../yardstick
tabulate==0.9.0
diff --git a/test/quality/test-db b/test/quality/test-db
new file mode 100644
index 00000000000..75c7e136081
--- /dev/null
+++ b/test/quality/test-db
@@ -0,0 +1 @@
+vulnerability-db_v6.0.2_2025-05-01T01:31:33Z_1746072708.tar.zst
diff --git a/test/quality/vulnerability-match-labels b/test/quality/vulnerability-match-labels
index e68f57ccf6f..b54f352b50b 160000
--- a/test/quality/vulnerability-match-labels
+++ b/test/quality/vulnerability-match-labels
@@ -1 +1 @@
-Subproject commit e68f57ccf6f2c627014cdca5e9777539525505ab
+Subproject commit b54f352b50b1c2bc8696cd5c10584d4905d5f209