diff --git a/.chronicle.yaml b/.chronicle.yaml new file mode 100644 index 00000000000..a2e6c04bd65 --- /dev/null +++ b/.chronicle.yaml @@ -0,0 +1,2 @@ +enforce-v0: true # don't make breaking-change label bump major version before 1.0. +title: "" diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index 70adc84d816..3fada0a417b 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -4,15 +4,11 @@ inputs: go-version: description: "Go version to install" required: true - default: "1.19.x" + default: "1.21.x" python-version: description: "Python version to install" required: true default: "3.10" - use-go-cache: - description: "Restore go cache" - required: true - default: "true" cache-key-prefix: description: "Prefix all cache keys with this value" required: true @@ -28,7 +24,8 @@ 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@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0 with: go-version: ${{ inputs.go-version }} @@ -54,38 +51,13 @@ runs: path: ${{ github.workspace }}/.tmp key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }} - # 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 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 }}- - - - name: (cache-miss) Bootstrap go dependencies + - name: Bootstrap go dependencies shell: bash - if: steps.go-mod-cache.outputs.cache-hit != 'true' && inputs.use-go-cache == 'true' run: make bootstrap-go - name: Install apt packages diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index df401370796..eb2c65955e7 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -7,4 +7,4 @@ updates: - package-ecosystem: "gomod" directory: "/" schedule: - interval: daily \ No newline at end of file + interval: daily diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c9e018fe09a..515fe648736 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Utilize Go Module Cache - uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v3.2.6 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 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@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.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@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 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@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/analyze@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 diff --git a/.github/workflows/oss-project-board-add.yaml b/.github/workflows/oss-project-board-add.yaml new file mode 100644 index 00000000000..f54cb6a0a02 --- /dev/null +++ b/.github/workflows/oss-project-board-add.yaml @@ -0,0 +1,19 @@ +name: Add to OSS board + +permissions: + contents: read + +on: + issues: + types: + - opened + - reopened + - transferred + - labeled + +jobs: + + run: + uses: "anchore/workflows/.github/workflows/oss-project-board-add.yaml@main" + secrets: + token: ${{ secrets.OSS_PROJECT_GH_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9d928aca3cf..843c7abfbc5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,16 +6,15 @@ 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 steps: - - uses: actions/checkout@v3 - + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Check if tag already exists # note: this will fail if the tag already exists run: | @@ -93,8 +92,9 @@ jobs: permissions: contents: write packages: write + id-token: write steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v2.5.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 with: fetch-depth: 0 @@ -105,21 +105,26 @@ jobs: build-cache-key-prefix: "snapshot" - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d #v3.0.0 with: username: ${{ secrets.TOOLBOX_DOCKER_USER }} password: ${{ secrets.TOOLBOX_DOCKER_PASS }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d #v3.0.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Cosign install + uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 #v3.1.2 + - 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 }} @@ -142,7 +147,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }} - - uses: anchore/sbom-action@4d571ad1038a9cc29d676154ef265ab8f9027042 # v0.14.2 + - uses: anchore/sbom-action@78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1 # v0.14.3 continue-on-error: true with: artifact-name: sbom.spdx.json @@ -156,8 +161,3 @@ jobs: env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} if: ${{ success() }} - - - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - path: dist/**/* - name: artifacts diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 0e43cc34f19..3d867a928fe 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # tag=v2.1.3 + uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0 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@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # tag=v1.0.26 + uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v1.0.26 with: sarif_file: results.sarif diff --git a/.github/workflows/update-bootstrap-tools.yml b/.github/workflows/update-bootstrap-tools.yml index e829989a74c..413bee511eb 100644 --- a/.github/workflows/update-bootstrap-tools.yml +++ b/.github/workflows/update-bootstrap-tools.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - GO_VERSION: "1.19.x" + GO_VERSION: "1.21.x" GO_STABLE_VERSION: true permissions: @@ -17,9 +17,9 @@ 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: go-version: ${{ env.GO_VERSION }} stable: ${{ env.GO_STABLE_VERSION }} @@ -31,6 +31,7 @@ jobs: 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') + QUILL_LATEST_VERSION=$(go list -m -json github.com/anchore/quill@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 @@ -40,6 +41,7 @@ jobs: 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/^(QUILL_VERSION := ).*/\1'${QUILL_LATEST_VERSION}'/' Makefile sed -r -i -e 's/^(GLOW_VERSION := ).*/\1'${GLOW_LATEST_VERSION}'/' Makefile # export the versions for use with create-pull-request @@ -49,16 +51,17 @@ jobs: echo "GORELEASER=$GORELEASER_LATEST_VERSION" >> $GITHUB_OUTPUT echo "GOSIMPORTS=$GOSIMPORTS_LATEST_VERSION" >> $GITHUB_OUTPUT echo "YAJSV=$YAJSV_LATEST_VERSION" >> $GITHUB_OUTPUT + echo "QUILL=$QUILL_LATEST_VERSION" >> $GITHUB_OUTPUT echo "GLOW=GLOW_LATEST_VERSION" >> $GITHUB_OUTPUT id: latest-versions - - 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@284f54f989303d2699d373481a0cfa13ad5a6666 # v5.0.1 + - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2 with: signoff: true delete-branch: true @@ -73,5 +76,7 @@ jobs: - [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 }}) + - [quill ${{ steps.latest-versions.outputs.QUILL }}](https://github.com/anchore/quill/releases/tag/${{ steps.latest-versions.outputs.QUILL }}) + - [glow ${{ steps.latest-versions.outputs.GLOW }}](https://github.com/charmbracelet/glow/releases/tag/${{ steps.latest-versions.outputs.GLOW }}) This is an auto-generated pull request to update all of the bootstrap tools to the latest versions. token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/update-syft-release.yml b/.github/workflows/update-syft-release.yml index 1215cff1b5f..232f58fb95f 100644 --- a/.github/workflows/update-syft-release.yml +++ b/.github/workflows/update-syft-release.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - GO_VERSION: "1.19.x" + GO_VERSION: "1.21.x" GO_STABLE_VERSION: true permissions: @@ -17,9 +17,9 @@ 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: go-version: ${{ env.GO_VERSION }} stable: ${{ env.GO_STABLE_VERSION }} @@ -38,13 +38,13 @@ jobs: # 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 + - 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@284f54f989303d2699d373481a0cfa13ad5a6666 # v5.0.1 + - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2 with: signoff: true delete-branch: true diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index edbe6ee72d5..6e5560912c8 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -16,7 +16,7 @@ jobs: name: "Static analysis" runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Bootstrap environment uses: ./.github/actions/bootstrap @@ -29,7 +29,7 @@ jobs: name: "Unit tests" runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: submodules: true @@ -53,13 +53,14 @@ jobs: run: make quality env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GRYPE_BY_CVE: "true" 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 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Bootstrap environment uses: ./.github/actions/bootstrap @@ -68,7 +69,7 @@ jobs: run: make validate-cyclonedx-schema - name: Restore integration test cache - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: ${{ github.workspace }}/test/integration/test-fixtures/cache key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }} @@ -80,7 +81,7 @@ jobs: name: "Build snapshot artifacts" runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Bootstrap environment uses: ./.github/actions/bootstrap @@ -99,7 +100,7 @@ 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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: snapshot key: snapshot-build-${{ github.run_id }} @@ -110,17 +111,17 @@ jobs: needs: [Build-Snapshot-Artifacts] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Download snapshot build - uses: actions/cache/restore@v3 + uses: actions/cache/restore@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: ${{ github.workspace }}/test/install/cache key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }} @@ -142,17 +143,17 @@ jobs: needs: [Build-Snapshot-Artifacts] runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Download snapshot build - uses: actions/cache/restore@v3 + uses: actions/cache/restore@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: image.tar key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }} @@ -167,19 +168,19 @@ jobs: needs: [Build-Snapshot-Artifacts] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Bootstrap environment uses: ./.github/actions/bootstrap - name: Restore CLI test-fixture cache - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: snapshot key: snapshot-build-${{ github.run_id }} diff --git a/.gitignore b/.gitignore index 038a3818f29..08749faa79b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ +/.tool-versions /go.work /go.work.sum +/.grype.yaml CHANGELOG.md -VERSION +/VERSION /snapshot/ /dist/ *.profile diff --git a/.golangci.yaml b/.golangci.yaml index 8e447bfc366..184f0f8ec2d 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -12,7 +12,6 @@ linters: enable: - asciicheck - bodyclose - - depguard - dogsled - dupl - errcheck @@ -57,6 +56,7 @@ run: # do not enable... # - deadcode # The owner seems to have abandoned the linter. Replaced by "unused". +# - depguard # we need to setup a configuration for this # - goprintffuncname # does not catch all cases and there are exceptions # - nakedret # does not catch all cases and should not fail a build # - gochecknoglobals diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 6c3274f6832..42efc0fdc26 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,6 +9,7 @@ env: builds: - id: linux-build + dir: ./cmd/grype binary: grype goos: - linux @@ -23,12 +24,13 @@ 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 binary: grype goos: - darwin @@ -44,6 +46,7 @@ builds: - QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log - id: windows-build + dir: ./cmd/grype binary: grype goos: - windows @@ -244,3 +247,16 @@ docker_manifests: - ghcr.io/anchore/grype:{{.Tag}}-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-s390x + +signs: + - cmd: 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 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 37fecf92b59..a978d888d01 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,57 @@ If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned. +## Setting up your environment + +Before you can contribute to Grype, you need to configure your development environment. + +### Debian setup + +You will need to install Go. The version on https://go.dev works best, using the system golang doesn't always work the way you might expect. + +Refer to the go.mod file in the root of this repo for the recommended version of Go to install. + +You will also need Docker. There's no reason the system packages shouldn't work, but we used the official Docker package. You can find instructions for installing Docker in Debian [here](https://docs.docker.com/engine/install/debian/). + +You also need to install some Debian packages + +```sh +sudo apt-get install build-essential git libxml2-utils +``` + +## Configuring Git + +You will need to configure your git client with your name and email address. This is easily done from the command line. + +```text +$ git config --global user.name "John Doe" +$ git config --global user.email "john.doe@example.com" +``` + +This username and email address will matter later in this guide. + +## Fork the repo + +You should fork the Grype repo using the "Fork" button at the top right of the Grype GitHub [site](https://github.com/anchore/grype/). You will be doing your development in your fork, then submit a pull request to Grype. There are many resources how to use GitHub effectively, we will not cover those here. + +## Adding a feature or fix + +If you look at the Grype [Issue](https://github.com/anchore/grype/issues) there are plenty of bugs and feature requests. Maybe look at the [good first issue](https://github.com/anchore/grype/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) list if you're not sure where to start. + +## Commit guidelines + +In the Grype project we like commits and pull requests (PR) to be easy to understand and review. Open source thrives best when everything happening is over documented and small enough to be understood. + +### Granular commits + +Please try to make every commit as simple as possible, but no simpler. The idea is that each commit should be a logical unit of code. Try not to commit too many tiny changes, for example every line changed in a file as a separate commit. And also try not to make a commit enormous, for example committing all your work at the end of the day. + +Rather than try to follow a strict guide on what is or is not best, we try to be flexible and simple in this space. Do what makes the most sense for the changes you are trying to include. + +### Commit title and description + +Remember that the message you leave for a commit is for the reviewer in the present, and for someone (maybe you) changing something in the future. Please make sure the title and description used is easy to understand and explains what was done. Jokes and clever comments generally don't age well in commit messages. Just the facts please. + ## Sign off your work The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0. @@ -70,18 +121,6 @@ Date: Mon Aug 1 11:27:13 2020 -0400 Signed-off-by: John Doe ``` - -[//]: # (TODO: Commit guidelines, granular commits) - - -[//]: # (TODO: Commit guidelines, descriptive messages) - - -[//]: # (TODO: Commit guidelines, commit title, extra body description) - - -[//]: # (TODO: PR title and description) - ## Test your changes This project has a `Makefile` which includes many helpers running both unit and integration tests. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. Ensure you've bootstrapped once before running tests: @@ -97,11 +136,30 @@ $ make unit $ make integration ``` +You can also run `make all` to run a more extensive test suite, but there is additional configuration that will be needed for those tests to run correctly. We will not cover the extra steps here. + +## Pull Request + +If you made it this far and all the tests are passing, it's time to submit a Pull Request (PR) for Grype. Submitting a PR is always a scary moment as what happens next can be an unknown. The Grype project strives to be easy to work with, we appreciate all contributions. Nobody is going to yell at you or try to make you feel bad. We love contributions and know how scary that first PR can be. + +### PR Title and Description + +Just like the commit title and description mentioned above, the PR title and description is very important for letting others know what's happening. Please include any details you think a reviewer will need to more properly review your PR. + +A PR that is very large or poorly described has a higher likelihood of being pushed to the end of the list. Reviewers like PRs they can understand and quickly review. + +### What to expect next + +Please be patient with the project. We try to review PRs in a timely manner, but this is highly dependent on all the other tasks we have going on. It's OK to ask for a status update every week or two, it's not OK to ask for a status update every day. + +It's very likely the reviewer will have questions and suggestions for changes to your PR. If your changes don't match the current style and flow of the other code, expect a request to change what you've done. + ## Document your changes -When proposed changes are modifying user-facing functionality or output, it is expected the PR will include updates to the documentation as well. +And lastly, when proposed changes are modifying user-facing functionality or output, it is expected the PR will include updates to the documentation as well. Grype is not a project that is heavy on documentation. This will mostly be updating the README and help for the tool. +If nobody knows new features exist, they can't use them! ## Security Vulnerabilities -Found a security vulnerability? See in our [Security Policy](SECURITY.md) to see how to report it to be solved as soon as possible. \ No newline at end of file +Found a security vulnerability? See in our [Security Policy](SECURITY.md) to see how to report it to be solved as soon as possible. diff --git a/DEVELOPING.md b/DEVELOPING.md index 5357f21caff..3c1993ecbb0 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -4,11 +4,9 @@ There are a few useful things to know before diving into the codebase. This proj ## Getting started -### Native Development - After cloning do the following: -1. run `go build main.go` to get a binary named `main` from the source (use `-o ` to get a differently named binary), or optionally `go run main.go` to run from source. +1. run `go build ./cmd/grype` to get a binary named `main` from the source (use `-o ` to get a differently named binary), or optionally `go run ./cmd/grype` to run from source. In order to run tests and build all artifacts: @@ -19,14 +17,6 @@ The main make tasks for common static analysis and testing are `lint`, `format`, See `make help` for all the current make tasks. -### Docker Development - -This depends on Docker and Docker Compose - -1. run `docker-compose build grype` to build the local development container -2. run `docker-compose run --rm grype bash` to enter into the container with all the bootstrapped dependencies installed. -3. run `make` to verify everything is installed and working properly - ## Relationship to Syft Grype uses Syft as a library for all-things related to obtaining and parsing the given scan target (pulling container @@ -41,7 +31,7 @@ to a released version (e.g. `go get github.com/anchore/syft@v` The currently supported database format is Sqlite3. Install `sqlite3` in your system and ensure that the `sqlite3` executable is available in your path. Ask `grype` about the location of the database, which will be different depending on the operating system: ``` -$ go run main.go db status +$ go run ./cmd/grype db status Location: /Users/alfredo/Library/Caches/grype/db Built: 2020-07-31 08:18:29 +0000 UTC Current DB Version: 1 diff --git a/Makefile b/Makefile index 72cded0a152..342d664ce7d 100644 --- a/Makefile +++ b/Makefile @@ -10,13 +10,13 @@ CHRONICLE_CMD = $(TEMP_DIR)/chronicle GLOW_CMD = $(TEMP_DIR)/glow # Tool versions ################################# -GOLANGCILINT_VERSION := v1.52.2 +GOLANGCILINT_VERSION := v1.54.2 GOSIMPORTS_VERSION := v0.3.8 BOUNCER_VERSION := v0.4.0 -CHRONICLE_VERSION := v0.6.0 -GORELEASER_VERSION := v1.18.2 +CHRONICLE_VERSION := v0.8.0 +GORELEASER_VERSION := v1.21.2 YAJSV_VERSION := v1.4.1 -QUILL_VERSION := v0.2.0 +QUILL_VERSION := v0.4.1 GLOW_VERSION := v1.5.1 SKOPEO_VERSION := v1.12.0 @@ -142,7 +142,7 @@ lint: ## Run gofmt + golangci lint checks @[ -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 ':')) + $(eval MALFORMED_FILENAMES := $(shell find . | grep -e ':' | grep -v -e "test/quality/.yardstick" -e "test/quality/vulnerability-match-labels")) @bash -c "[[ '$(MALFORMED_FILENAMES)' == '' ]] || (printf '\nfound unsupported filename characters:\n$(MALFORMED_FILENAMES)\n\n' && false)" .PHONY: format @@ -170,13 +170,14 @@ check-go-mod-tidy: .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) + go test -race -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 + go run cmd/grype/main.go alpine:latest .PHONY: quality quality: ## Run quality tests diff --git a/README.md b/README.md index b3512974317..879fc8f652f 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,14 @@

[![Static Analysis + Unit + Integration](https://github.com/anchore/grype/workflows/Static%20Analysis%20+%20Unit%20+%20Integration/badge.svg)](https://github.com/anchore/grype/actions?query=workflow%3A%22Static+Analysis+%2B+Unit+%2B+Integration%22) -[![Acceptance](https://github.com/anchore/grype/workflows/Acceptance/badge.svg)](https://github.com/anchore/grype/actions?query=workflow%3AAcceptance) +![Validations](https://github.com/anchore/grype/workflows/Validations/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/anchore/grype)](https://goreportcard.com/report/github.com/anchore/grype) [![GitHub release](https://img.shields.io/github/release/anchore/grype.svg)](https://github.com/anchore/grype/releases/latest) [![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/anchore/grype.svg)](https://github.com/anchore/grype) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/grype/blob/main/LICENSE) [![Slack Invite](https://img.shields.io/badge/Slack-Join-blue?logo=slack)](https://anchore.com/slack) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/anchore/grype/badge)](https://api.securityscorecards.dev/projects/github.com/anchore/grype) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/6708/badge)](https://www.bestpractices.dev/projects/6708) 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. @@ -46,6 +47,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). @@ -88,6 +90,34 @@ See [DEVELOPING.md](DEVELOPING.md#native-development) for instructions to build 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. +## 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/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 [Install the binary](#installation), and make sure that `grype` is available in your path. To scan for vulnerabilities in an image: @@ -159,14 +189,21 @@ 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 ``` +### Supported versions + +Any version of Grype before v0.40.1 is not supported. Unsupported releases will not receive any software updates or +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. + ### 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: @@ -267,20 +304,9 @@ Grype lets you define custom output formats, using [Go templates](https://golang - Grype's template processing uses the same data models as the `json` output format — so if you're wondering what data is available as you author a template, you can use the output from `grype -o json` as a reference. -**Example:** You could make Grype output data in CSV format by writing a Go template that renders CSV data and then running `grype -o template -t ~/path/to/csv.tmpl`. - **Please note:** Templates can access information about the system they are running on, such as environment variables. You should never run untrusted templates. -Here's what the `csv.tmpl` file might look like: - -```gotemplate -"Package","Version Installed","Vulnerability ID","Severity" -{{- range .Matches}} -"{{.Artifact.Name}}","{{.Artifact.Version}}","{{.Vulnerability.ID}}","{{.Vulnerability.Severity}}" -{{- end}} -``` - -Which would produce output like: +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" @@ -290,6 +316,8 @@ Which would produce output like: ... ``` +You can also find the template for the default "table" output format in the same place. + Grype also includes a vast array of utility templating functions from [sprig](http://masterminds.github.io/sprig/) apart from the default golang [text/template](https://pkg.go.dev/text/template#hdr-Functions) to allow users to customize the output from Grype. ### Gating on severity of vulnerabilities @@ -324,6 +352,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 @@ -370,7 +401,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 @@ -455,7 +558,7 @@ Find complete information on Grype's database commands by running `grype db --he Grype supplies shell completion through its CLI implementation ([cobra](https://github.com/spf13/cobra/blob/master/shell_completions.md)). Generate the completion code for your shell by running one of the following commands: - `grype completion ` -- `go run main.go completion ` +- `go run ./cmd/grype completion ` This will output a shell script to STDOUT, which can then be used as a completion script for Grype. Running one of the above commands with the `-h` or `--help` flags will provide instructions on how to do that for your chosen shell. @@ -653,23 +756,41 @@ 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 + # filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate + # GRYPE_REGISTRY_CA_CERT env var + ca-cert: "" + # 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 + # the URL to the registry (e.g. "docker.io", "localhost:5000", etc.) + # GRYPE_REGISTRY_AUTH_AUTHORITY env var + - authority: "" + + # GRYPE_REGISTRY_AUTH_USERNAME env var username: "" - # same as GRYPE_REGISTRY_AUTH_PASSWORD env var + + # GRYPE_REGISTRY_AUTH_PASSWORD env var password: "" + # note: token and username/password are mutually exclusive - # same as GRYPE_REGISTRY_AUTH_TOKEN env var + # GRYPE_REGISTRY_AUTH_TOKEN env var token: "" - - ... # note, more credentials can be provided via config file only + + # filepath to the client certificate used for TLS authentication to the registry + # GRYPE_REGISTRY_AUTH_TLS_CERT env var + tls-cert: "" + + # filepath to the client key used for TLS authentication to the registry + # GRYPE_REGISTRY_AUTH_TLS_KEY env var + tls-key: "" + + # - ... # note, more credentials can be provided via config file only (not env vars) + log: # use structured logging diff --git a/cmd/cmd.go b/cmd/cmd.go deleted file mode 100644 index 20b4c14ebb1..00000000000 --- a/cmd/cmd.go +++ /dev/null @@ -1,132 +0,0 @@ -package cmd - -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/db.go b/cmd/db.go deleted file mode 100644 index 9a56d61726a..00000000000 --- a/cmd/db.go +++ /dev/null @@ -1,14 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -var dbCmd = &cobra.Command{ - Use: "db", - Short: "vulnerability database operations", -} - -func init() { - rootCmd.AddCommand(dbCmd) -} diff --git a/cmd/db_delete.go b/cmd/db_delete.go deleted file mode 100644 index ff404ab7000..00000000000 --- a/cmd/db_delete.go +++ /dev/null @@ -1,33 +0,0 @@ -package cmd - -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/db_diff.go b/cmd/db_diff.go deleted file mode 100644 index 6f81455e95c..00000000000 --- a/cmd/db_diff.go +++ /dev/null @@ -1,151 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - "github.com/wagoodman/go-partybus" - - "github.com/anchore/grype/grype/db" - "github.com/anchore/grype/grype/differ" - "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/internal/bus" - "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/internal/ui" - "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) - 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 - } - - if len(*diff) == 0 { - fmt.Println("Databases are identical!") - } else { - err := d.Present(dbDiffOutputFormat, diff, os.Stdout) - if err != nil { - errs <- err - } - } - - if deleteDatabases { - errs <- d.DeleteDatabases() - } - - bus.Publish(partybus.Event{ - Type: event.NonRootCommandFinished, - Value: "", - }) - }() - return errs -} - -func runDBDiffCmd(cmd *cobra.Command, args []string) error { - reporter, closer, err := reportWriter() - defer func() { - if err := closer(); err != nil { - log.Warnf("unable to write to report destination: %+v", err) - } - }() - if err != nil { - return err - } - - 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, reporter)..., - ) -} - -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/db_import.go b/cmd/db_import.go deleted file mode 100644 index fc00261da4b..00000000000 --- a/cmd/db_import.go +++ /dev/null @@ -1,35 +0,0 @@ -package cmd - -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/db_status.go b/cmd/db_status.go deleted file mode 100644 index 891c6c08e27..00000000000 --- a/cmd/db_status.go +++ /dev/null @@ -1,42 +0,0 @@ -package cmd - -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/db_update.go b/cmd/db_update.go deleted file mode 100644 index d697d68f6e5..00000000000 --- a/cmd/db_update.go +++ /dev/null @@ -1,72 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" - "github.com/wagoodman/go-partybus" - - "github.com/anchore/grype/grype/db" - "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/internal/bus" - "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/internal/ui" - "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) - 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.Publish(partybus.Event{ - Type: event.NonRootCommandFinished, - Value: result, - }) - }() - return errs -} - -func runDBUpdateCmd(_ *cobra.Command, _ []string) error { - reporter, closer, err := reportWriter() - defer func() { - if err := closer(); err != nil { - log.Warnf("unable to write to report destination: %+v", err) - } - }() - if err != nil { - return err - } - return eventLoop( - startDBUpdateCmd(), - setupSignals(), - eventSubscription, - stereoscope.Cleanup, - ui.Select(isVerbose(), appConfig.Quiet, reporter)..., - ) -} diff --git a/cmd/event_loop.go b/cmd/event_loop.go deleted file mode 100644 index 596f8b52c2a..00000000000 --- a/cmd/event_loop.go +++ /dev/null @@ -1,98 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "os" - - "github.com/hashicorp/go-multierror" - "github.com/wagoodman/go-partybus" - - "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/internal/ui" -) - -// 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 ...ui.UI) error { - defer cleanupFn() - events := subscription.Events() - var err error - var ux ui.UI - - if ux, err = setupUI(subscription.Unsubscribe, 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(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) { - for _, ux := range uis { - if err := ux.Setup(unsubscribe); 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/event_loop_test.go b/cmd/event_loop_test.go deleted file mode 100644 index c860e433ae0..00000000000 --- a/cmd/event_loop_test.go +++ /dev/null @@ -1,456 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "syscall" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/wagoodman/go-partybus" - - "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/internal/ui" -) - -var _ ui.UI = (*uiMock)(nil) - -type uiMock struct { - t *testing.T - finalEvent partybus.Event - unsubscribe func() error - mock.Mock -} - -func (u *uiMock) Setup(unsubscribe func() error) error { - u.t.Logf("UI Setup called") - u.unsubscribe = unsubscribe - return u.Called(unsubscribe).Error(0) -} - -func (u *uiMock) Handle(event partybus.Event) error { - u.t.Logf("UI Handle called: %+v", event.Type) - if event == u.finalEvent { - assert.NoError(u.t, u.unsubscribe()) - } - return u.Called(event).Error(0) -} - -func (u *uiMock) Teardown(_ bool) error { - 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.VulnerabilityScanningFinished, - } - - 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.VulnerabilityScanningFinished, - } - - 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.VulnerabilityScanningFinished, - Error: fmt.Errorf("unable to create presenter"), - } - - 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.VulnerabilityScanningFinished, - } - - 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/cli.go b/cmd/grype/cli/cli.go new file mode 100644 index 00000000000..7f89528f045 --- /dev/null +++ b/cmd/grype/cli/cli.go @@ -0,0 +1,109 @@ +package cli + +import ( + "os" + "runtime/debug" + + "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" + "github.com/anchore/grype/grype/vulnerability" + "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 create(id clio.Identification) (clio.Application, *cobra.Command) { + clioCfg := 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.UI, error) { + noUI := ui.None(cfg.Log.Quiet) + if !cfg.Log.AllowUI(os.Stdin) || cfg.Log.Quiet { + return []clio.UI{noUI}, nil + } + + return []clio.UI{ + 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(state *clio.State, err error) { + stereoscope.Cleanup() + }) + + app := clio.New(*clioCfg) + + rootCmd := commands.Root(app) + + // add sub-commands + rootCmd.AddCommand( + commands.DB(app), + commands.Completion(), + commands.Explain(app), + clio.VersionCommand(id, syftVersion, dbVersion), + ) + + 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", vulnerability.SchemaVersion +} 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/completion.go b/cmd/grype/cli/commands/completion.go similarity index 75% rename from cmd/completion.go rename to cmd/grype/cli/commands/completion.go index 1ba1bb0fb34..9a68998821f 100644 --- a/cmd/completion.go +++ b/cmd/grype/cli/commands/completion.go @@ -1,4 +1,4 @@ -package cmd +package commands import ( "context" @@ -11,11 +11,12 @@ import ( "github.com/spf13/cobra" ) -// 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() *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 +47,22 @@ $ 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"}, + 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) { @@ -108,3 +91,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..30e611c3342 --- /dev/null +++ b/cmd/grype/cli/commands/db.go @@ -0,0 +1,37 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/anchore/clio" + "github.com/anchore/grype/cmd/grype/cli/options" +) + +type DBOptions struct { + DB options.Database `yaml:"db" json:"db" mapstructure:"db"` +} + +func dbOptionsDefault(id clio.Identification) *DBOptions { + return &DBOptions{ + DB: options.DefaultDatabase(id), + } +} + +func DB(app clio.Application) *cobra.Command { + db := &cobra.Command{ + Use: "db", + Short: "vulnerability database operations", + } + + db.AddCommand( + DBCheck(app), + DBDelete(app), + DBDiff(app), + DBImport(app), + DBList(app), + DBStatus(app), + DBUpdate(app), + ) + + return db +} diff --git a/cmd/db_check.go b/cmd/grype/cli/commands/db_check.go similarity index 56% rename from cmd/db_check.go rename to cmd/grype/cli/commands/db_check.go index 94f9bcec313..54768b407f2 100644 --- a/cmd/db_check.go +++ b/cmd/grype/cli/commands/db_check.go @@ -1,26 +1,35 @@ -package cmd +package commands import ( "fmt" + "os" "github.com/spf13/cobra" + "github.com/anchore/clio" + "github.com/anchore/grype/cmd/grype/cli/options" "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, -} +const ( + exitCodeOnDBUpgradeAvailable = 100 +) -func init() { - dbCmd.AddCommand(dbCheckCmd) +func DBCheck(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&cobra.Command{ + Use: "check", + Short: "check to see if there is a database update available", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBCheck(opts.DB) + }, + }, opts) } -func runDBCheckCmd(_ *cobra.Command, _ []string) error { - dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig()) +func runDBCheck(opts options.Database) error { + dbCurator, err := db.NewCurator(opts.ToCuratorConfig()) if err != nil { return err } @@ -44,5 +53,7 @@ func runDBCheckCmd(_ *cobra.Command, _ []string) error { fmt.Printf("Updated DB URL: %s\n", updateDBEntry.URL.String()) fmt.Println("You can run 'grype db update' to update to the latest db") + os.Exit(exitCodeOnDBUpgradeAvailable) //nolint:gocritic + return nil } diff --git a/cmd/grype/cli/commands/db_delete.go b/cmd/grype/cli/commands/db_delete.go new file mode 100644 index 00000000000..57b1b00644d --- /dev/null +++ b/cmd/grype/cli/commands/db_delete.go @@ -0,0 +1,37 @@ +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" +) + +func DBDelete(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&cobra.Command{ + Use: "delete", + Short: "delete the vulnerability database", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBDelete(opts.DB) + }, + }, opts) +} + +func runDBDelete(opts options.Database) error { + dbCurator, err := db.NewCurator(opts.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/commands/db_diff.go b/cmd/grype/cli/commands/db_diff.go new file mode 100644 index 00000000000..03b074bf9ba --- /dev/null +++ b/cmd/grype/cli/commands/db_diff.go @@ -0,0 +1,127 @@ +package commands + +import ( + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/spf13/cobra" + + "github.com/anchore/clio" + "github.com/anchore/grype/cmd/grype/cli/options" + "github.com/anchore/grype/grype/db" + "github.com/anchore/grype/grype/differ" + "github.com/anchore/grype/internal/bus" + "github.com/anchore/grype/internal/log" +) + +type dbDiffOptions struct { + Output string `yaml:"output" json:"output" mapstructure:"output"` + Delete bool `yaml:"delete" json:"delete" mapstructure:"delete"` + DBOptions `yaml:",inline" mapstructure:",squash"` +} + +var _ clio.FlagAdder = (*dbDiffOptions)(nil) + +func (d *dbDiffOptions) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[table, json])") + flags.BoolVarP(&d.Delete, "delete", "d", "delete downloaded databases after diff occurs") +} + +func DBDiff(app clio.Application) *cobra.Command { + opts := &dbDiffOptions{ + Output: "table", + DBOptions: *dbOptionsDefault(app.ID()), + } + + return app.SetupCommand(&cobra.Command{ + Use: "diff [flags] base_db_url target_db_url", + Short: "diff two DBs and display the result", + Args: cobra.MaximumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) (err error) { + 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(opts.DB) + if err != nil { + return err + } + case 1: + log.Info("target_db_url not provided; fetching most recent") + base = args[0] + _, target, err = getDefaultURLs(opts.DB) + if err != nil { + return err + } + default: + base = args[0] + target = args[1] + } + + return runDBDiff(opts, base, target) + }, + }, opts) +} + +func runDBDiff(opts *dbDiffOptions, base string, target string) (errs error) { + d, err := differ.NewDiffer(opts.DB.ToCuratorConfig()) + if err != nil { + return err + } + + if err := d.SetBaseDB(base); err != nil { + return err + } + + if err := d.SetTargetDB(target); err != nil { + return err + } + + diff, err := d.DiffDatabases() + if err != nil { + return err + } + + sb := &strings.Builder{} + + if len(*diff) == 0 { + sb.WriteString("Databases are identical!\n") + } else { + err := d.Present(opts.Output, diff, sb) + if err != nil { + errs = multierror.Append(errs, err) + } + } + + bus.Report(sb.String()) + + if opts.Delete { + errs = multierror.Append(errs, d.DeleteDatabases()) + } + + return errs +} + +func getDefaultURLs(opts options.Database) (baseURL string, targetURL string, err error) { + dbCurator, err := db.NewCurator(opts.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/commands/db_import.go b/cmd/grype/cli/commands/db_import.go new file mode 100644 index 00000000000..cb98c3501da --- /dev/null +++ b/cmd/grype/cli/commands/db_import.go @@ -0,0 +1,39 @@ +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" + "github.com/anchore/grype/internal" +) + +func DBImport(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&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: func(cmd *cobra.Command, args []string) error { + return runDBImport(opts.DB, args[0]) + }, + }, opts) +} + +func runDBImport(opts options.Database, dbArchivePath string) error { + dbCurator, err := db.NewCurator(opts.ToCuratorConfig()) + if err != nil { + return err + } + + if err := dbCurator.ImportFrom(dbArchivePath); err != nil { + return fmt.Errorf("unable to import vulnerability database: %+v", err) + } + + return stderrPrintLnf("Vulnerability database imported") +} diff --git a/cmd/db_list.go b/cmd/grype/cli/commands/db_list.go similarity index 57% rename from cmd/db_list.go rename to cmd/grype/cli/commands/db_list.go index 7fcc2880d4c..ca1ace5c736 100644 --- a/cmd/db_list.go +++ b/cmd/grype/cli/commands/db_list.go @@ -1,4 +1,4 @@ -package cmd +package commands import ( "encoding/json" @@ -7,26 +7,39 @@ import ( "github.com/spf13/cobra" + "github.com/anchore/clio" "github.com/anchore/grype/grype/db" ) -var dbListOutputFormat string +type dbListOptions struct { + Output string `yaml:"output" json:"output" mapstructure:"output"` + DBOptions `yaml:",inline" mapstructure:",squash"` +} + +var _ clio.FlagAdder = (*dbListOptions)(nil) -var dbListCmd = &cobra.Command{ - Use: "list", - Short: "list all DBs available according to the listing URL", - Args: cobra.ExactArgs(0), - RunE: runDBListCmd, +func (d *dbListOptions) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, raw, json])") } -func init() { - dbListCmd.Flags().StringVarP(&dbListOutputFormat, "output", "o", "text", "format to display results (available=[text, raw, json])") +func DBList(app clio.Application) *cobra.Command { + opts := &dbListOptions{ + Output: "text", + DBOptions: *dbOptionsDefault(app.ID()), + } - dbCmd.AddCommand(dbListCmd) + return app.SetupCommand(&cobra.Command{ + Use: "list", + Short: "list all DBs available according to the listing URL", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBList(opts) + }, + }, opts) } -func runDBListCmd(_ *cobra.Command, _ []string) error { - dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig()) +func runDBList(opts *dbListOptions) error { + dbCurator, err := db.NewCurator(opts.DB.ToCuratorConfig()) if err != nil { return err } @@ -43,7 +56,7 @@ func runDBListCmd(_ *cobra.Command, _ []string) error { return stderrPrintLnf("No databases available for the current schema (%d)", supportedSchema) } - switch dbListOutputFormat { + switch opts.Output { case "text": // summarize each listing entry for the current DB schema for _, l := range available { @@ -70,7 +83,7 @@ func runDBListCmd(_ *cobra.Command, _ []string) error { return fmt.Errorf("failed to db listing information: %+v", err) } default: - return fmt.Errorf("unsupported output format: %s", dbListOutputFormat) + return fmt.Errorf("unsupported output format: %s", opts.Output) } return nil diff --git a/cmd/grype/cli/commands/db_status.go b/cmd/grype/cli/commands/db_status.go new file mode 100644 index 00000000000..5dbfb2e749f --- /dev/null +++ b/cmd/grype/cli/commands/db_status.go @@ -0,0 +1,46 @@ +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" +) + +func DBStatus(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&cobra.Command{ + Use: "status", + Short: "display database status", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBStatus(opts.DB) + }, + }, opts) +} + +func runDBStatus(opts options.Database) error { + dbCurator, err := db.NewCurator(opts.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/commands/db_update.go b/cmd/grype/cli/commands/db_update.go new file mode 100644 index 00000000000..e395d50862f --- /dev/null +++ b/cmd/grype/cli/commands/db_update.go @@ -0,0 +1,48 @@ +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" + "github.com/anchore/grype/internal/bus" + "github.com/anchore/grype/internal/log" +) + +func DBUpdate(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&cobra.Command{ + Use: "update", + Short: "download the latest vulnerability database", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBUpdate(opts.DB) + }, + }, opts) +} + +func runDBUpdate(opts options.Database) error { + dbCurator, err := db.NewCurator(opts.ToCuratorConfig()) + if err != nil { + return err + } + updated, err := dbCurator.Update() + if err != nil { + return 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" + } + + 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..8b0368b771f --- /dev/null +++ b/cmd/grype/cli/commands/explain.go @@ -0,0 +1,56 @@ +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{} + + return app.SetupCommand(&cobra.Command{ + Use: "explain --id [VULNERABILITY ID]", + Short: "Ask grype to explain a set of findings", + RunE: func(cmd *cobra.Command, args []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 ...'") + }, + }, opts) +} diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go new file mode 100644 index 00000000000..edc4fb7a584 --- /dev/null +++ b/cmd/grype/cli/commands/root.go @@ -0,0 +1,356 @@ +package commands + +import ( + "errors" + "fmt" + "strings" + + "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/db" + grypeDb "github.com/anchore/grype/grype/db/v5" + "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/store" + "github.com/anchore/grype/grype/vex" + "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/linux" + 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}} 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": app.ID().Name, + }), + Args: validateRootArgs, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *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(grypeDb.NotFixedState)}, + {FixState: string(grypeDb.WontFixState)}, + {FixState: string(grypeDb.UnknownFixState)}, +} + +var ignoreFixedMatches = []match.IgnoreRule{ + {FixState: string(grypeDb.FixedState)}, +} + +var ignoreVEXFixedNotAffected = []match.IgnoreRule{ + {VexStatus: string(vex.StatusNotAffected)}, + {VexStatus: string(vex.StatusFixed)}, +} + +//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, + }) + if err != nil { + return err + } + + var str *store.Store + var status *db.Status + var dbCloser *db.Closer + 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...) + } + + for _, ignoreState := range stringutil.SplitCommaSeparatedString(opts.IgnoreStates) { + switch grypeDb.FixState(ignoreState) { + case grypeDb.UnknownFixState, grypeDb.FixedState, grypeDb.NotFixedState, grypeDb.WontFixState: + 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) { + log.Debug("loading DB") + str, status, dbCloser, err = grype.LoadVulnerabilityDB(opts.DB.ToCuratorConfig(), opts.DB.AutoUpdate) + return validateDBLoad(err, status) + }, + func() (err error) { + 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 + } + + if dbCloser != nil { + defer dbCloser.Close() + } + + if err = applyVexRules(opts); err != nil { + return fmt.Errorf("applying vex rules: %w", err) + } + + applyDistroHint(packages, &pkgContext, opts) + + vulnMatcher := grype.VulnerabilityMatcher{ + Store: *str, + IgnoreRules: opts.Ignore, + NormalizeByCVE: opts.ByCVE, + FailSeverity: opts.FailOnServerity(), + 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) + } + + if err = writer.Write(models.PresenterConfig{ + ID: app.ID(), + Matches: *remainingMatches, + IgnoredMatches: ignoredMatches, + Packages: packages, + Context: pkgContext, + MetadataProvider: str, + SBOM: s, + AppConfig: opts, + DBStatus: status, + }); err != nil { + errs = appendErrors(errs, err) + } + + return errs +} + +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] + } + 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(id clio.Identification, opts *options.Grype) { + if !opts.CheckForAppUpdate { + return + } + + version := id.Version + isAvailable, newVersion, err := isUpdateAvailable(version) + if err != nil { + log.Errorf(err.Error()) + } + if isAvailable { + log.Infof("new version of %s is available: %s (currently running: %s)", id.Name, newVersion, version) + + bus.Publish(partybus.Event{ + Type: event.CLIAppUpdateAvailable, + Value: parsers.UpdateCheck{ + New: newVersion, + Current: id.Version, + }, + }) + } else { + log.Debugf("no new %s update available", id.Name) + } +} + +func getMatchers(opts *options.Grype) []matcher.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(opts.Match.Golang), + Stock: stock.MatcherConfig(opts.Match.Stock), + }, + ) +} + +func getProviderConfig(opts *options.Grype) pkg.ProviderConfig { + return pkg.ProviderConfig{ + SyftProviderConfig: pkg.SyftProviderConfig{ + RegistryOptions: opts.Registry.ToOptions(), + Exclusions: opts.Exclusions, + CatalogingOptions: opts.Search.ToConfig(), + Platform: opts.Platform, + Name: opts.Name, + DefaultImagePullSource: opts.DefaultImagePullSource, + }, + SynthesisConfig: pkg.SynthesisConfig{ + GenerateMissingCPEs: opts.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) +} + +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/root_test.go b/cmd/grype/cli/commands/root_test.go similarity index 91% rename from cmd/root_test.go rename to cmd/grype/cli/commands/root_test.go index 70f85c159a6..58a2bd3a75c 100644 --- a/cmd/root_test.go +++ b/cmd/grype/cli/commands/root_test.go @@ -1,17 +1,17 @@ -package cmd +package commands import ( "testing" "github.com/stretchr/testify/assert" + "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal/config" ) func Test_applyDistroHint(t *testing.T) { ctx := pkg.Context{} - cfg := config.Application{} + cfg := options.Grype{} applyDistroHint([]pkg.Package{}, &ctx, &cfg) assert.Nil(t, ctx.Distro) diff --git a/internal/version/update.go b/cmd/grype/cli/commands/update.go similarity index 78% rename from internal/version/update.go rename to cmd/grype/cli/commands/update.go index 03c003f2cb0..2916a588faf 100644 --- a/internal/version/update.go +++ b/cmd/grype/cli/commands/update.go @@ -1,4 +1,4 @@ -package version +package commands import ( "fmt" @@ -7,7 +7,7 @@ import ( "strings" hashiVersion "github.com/anchore/go-version" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/cmd/grype/internal" ) var latestAppVersionURL = struct { @@ -15,16 +15,22 @@ 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(version string) (bool, string, error) { + if !isProductionBuild(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(version) if err != nil { return false, "", fmt.Errorf("failed to parse current application version: %w", err) } diff --git a/internal/version/update_test.go b/cmd/grype/cli/commands/update_test.go similarity index 95% rename from internal/version/update_test.go rename to cmd/grype/cli/commands/update_test.go index 10c34b7383a..eb2dedb9fcc 100644 --- a/internal/version/update_test.go +++ b/cmd/grype/cli/commands/update_test.go @@ -1,4 +1,4 @@ -package version +package commands import ( "net/http" @@ -6,6 +6,7 @@ import ( "testing" hashiVersion "github.com/anchore/go-version" + "github.com/anchore/grype/cmd/grype/internal" ) func TestIsUpdateAvailable(t *testing.T) { @@ -74,7 +75,7 @@ func TestIsUpdateAvailable(t *testing.T) { }, { name: "NoBuildVersion", - buildVersion: valueNotProvided, + buildVersion: internal.NotProvided, latestVersion: "1.0.0", code: 200, isAvailable: false, @@ -96,7 +97,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 +108,7 @@ func TestIsUpdateAvailable(t *testing.T) { latestAppVersionURL.host = mockSrv.URL defer mockSrv.Close() - isAvailable, newVersion, err := IsUpdateAvailable() + isAvailable, newVersion, err := isUpdateAvailable(version) if err != nil && !test.err { t.Fatalf("got error but expected none: %+v", err) } else if err == nil && test.err { diff --git a/cmd/grype/cli/commands/util.go b/cmd/grype/cli/commands/util.go new file mode 100644 index 00000000000..f587f33c1cc --- /dev/null +++ b/cmd/grype/cli/commands/util.go @@ -0,0 +1,68 @@ +package commands + +import ( + "fmt" + "os" + "strings" + "sync" + + "github.com/hashicorp/go-multierror" + "golang.org/x/exp/maps" +) + +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...) +} diff --git a/cmd/grype/cli/commands/util_test.go b/cmd/grype/cli/commands/util_test.go new file mode 100644 index 00000000000..fb04777c0db --- /dev/null +++ b/cmd/grype/cli/commands/util_test.go @@ -0,0 +1,165 @@ +package commands + +import ( + "fmt" + "sync" + "sync/atomic" + "testing" + + "github.com/hashicorp/go-multierror" + "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) + }) + } +} diff --git a/internal/config/database.go b/cmd/grype/cli/options/database.go similarity index 66% rename from internal/config/database.go rename to cmd/grype/cli/options/database.go index a3854297c5a..83fd1770b3c 100644 --- a/internal/config/database.go +++ b/cmd/grype/cli/options/database.go @@ -1,17 +1,17 @@ -package config +package options import ( "path" "time" "github.com/adrg/xdg" - "github.com/spf13/viper" + "github.com/anchore/clio" "github.com/anchore/grype/grype/db" "github.com/anchore/grype/internal" ) -type database struct { +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"` @@ -21,18 +21,18 @@ type database struct { 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 DefaultDatabase(id clio.Identification) Database { + return Database{ + Dir: path.Join(xdg.CacheHome, id.Name, "db"), + UpdateURL: internal.DBUpdateURL, + AutoUpdate: true, + ValidateAge: true, + // After this period (5 days) the db data is considered stale + MaxAllowedBuiltAge: time.Hour * 24 * 5, + } } -func (cfg database) ToCuratorConfig() db.Config { +func (cfg Database) ToCuratorConfig() db.Config { return db.Config{ DBRootDir: cfg.Dir, ListingURL: cfg.UpdateURL, diff --git a/internal/config/datasources.go b/cmd/grype/cli/options/datasources.go similarity index 68% rename from internal/config/datasources.go rename to cmd/grype/cli/options/datasources.go index df0dc3c1700..99f1543bc8a 100644 --- a/internal/config/datasources.go +++ b/cmd/grype/cli/options/datasources.go @@ -1,8 +1,6 @@ -package config +package options import ( - "github.com/spf13/viper" - "github.com/anchore/grype/grype/matcher/java" ) @@ -12,7 +10,7 @@ const ( type externalSources struct { Enable bool `yaml:"enable" json:"enable" mapstructure:"enable"` - Maven maven `yaml:"maven" json:"maven" mapsructure:"maven"` + Maven maven `yaml:"maven" json:"maven" mapstructure:"maven"` } type maven struct { @@ -20,10 +18,13 @@ type maven struct { 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 defaultExternalSources() externalSources { + return externalSources{ + Maven: maven{ + SearchUpstreamBySha1: true, + BaseURL: defaultMavenBaseURL, + }, + } } func (cfg externalSources) ToJavaMatcherConfig() java.ExternalSearchConfig { diff --git a/cmd/grype/cli/options/grype.go b/cmd/grype/cli/options/grype.go new file mode 100644 index 00000000000..8219d77c369 --- /dev/null +++ b/cmd/grype/cli/options/grype.go @@ -0,0 +1,151 @@ +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 + 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"` + 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"` + 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 + 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 +} + +var _ interface { + clio.FlagAdder + clio.PostLoader +} = (*Grype)(nil) + +func DefaultGrype(id clio.Identification) *Grype { + return &Grype{ + Search: defaultSearch(source.SquashedScope), + DB: DefaultDatabase(id), + Match: defaultMatchConfig(), + ExternalSources: defaultExternalSources(), + CheckForAppUpdate: true, + VexAdd: []string{}, + } +} + +// 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.FailOnServerity() + if failOnSeverity == vulnerability.UnknownSeverity { + return fmt.Errorf("bad --fail-on severity value '%s'", o.FailOn) + } + } + return nil +} + +func (o Grype) FailOnServerity() *vulnerability.Severity { + severity := vulnerability.ParseSeverity(o.FailOn) + return &severity +} diff --git a/internal/config/match.go b/cmd/grype/cli/options/match.go similarity index 70% rename from internal/config/match.go rename to cmd/grype/cli/options/match.go index e855b77a9b4..47dc715dff5 100644 --- a/internal/config/match.go +++ b/cmd/grype/cli/options/match.go @@ -1,8 +1,4 @@ -package config - -import ( - "github.com/spf13/viper" -) +package options // matchConfig contains all matching-related configuration options available to the user via the application config. type matchConfig struct { @@ -12,6 +8,7 @@ type matchConfig struct { 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 } @@ -19,12 +16,17 @@ 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) +func defaultMatchConfig() matchConfig { + useCpe := matcherConfig{UseCPEs: true} + dontUseCpe := matcherConfig{UseCPEs: false} + return matchConfig{ + Java: dontUseCpe, + Dotnet: dontUseCpe, + Golang: dontUseCpe, + Javascript: dontUseCpe, + Python: dontUseCpe, + Ruby: dontUseCpe, + Rust: dontUseCpe, + Stock: useCpe, + } } diff --git a/cmd/grype/cli/options/registry.go b/cmd/grype/cli/options/registry.go new file mode 100644 index 00000000000..4723fb8c467 --- /dev/null +++ b/cmd/grype/cli/options/registry.go @@ -0,0 +1,82 @@ +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 _ clio.PostLoader = (*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 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..c79190afccd --- /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/pkg/cataloger" + "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 _ clio.PostLoader = (*search)(nil) + +func defaultSearch(scope source.Scope) search { + c := cataloger.DefaultSearchConfig() + 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) GetScope() source.Scope { + return source.ParseScope(cfg.Scope) +} + +func (cfg search) ToConfig() cataloger.Config { + return cataloger.Config{ + Search: cataloger.SearchConfig{ + IncludeIndexedArchives: cfg.IncludeIndexedArchives, + IncludeUnindexedArchives: cfg.IncludeUnindexedArchives, + Scope: cfg.GetScope(), + }, + ExcludeBinaryOverlapByOwnership: true, + } +} 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/ui/__snapshots__/handle_database_diff_started_test.snap b/cmd/grype/cli/ui/__snapshots__/handle_database_diff_started_test.snap new file mode 100755 index 00000000000..9386bb14a93 --- /dev/null +++ b/cmd/grype/cli/ui/__snapshots__/handle_database_diff_started_test.snap @@ -0,0 +1,8 @@ + +[TestHandler_handleDatabaseDiffStarted/DB_diff_started - 1] + ⠋ Comparing Vulnerability DBs ━━━━━━━━━━━━━━━━━━━━ [current] +--- + +[TestHandler_handleDatabaseDiffStarted/DB_diff_complete - 1] + ✔ Compared Vulnerability DBs [20 differences found] +--- diff --git a/cmd/grype/cli/ui/__snapshots__/handle_update_vulnerability_database_test.snap b/cmd/grype/cli/ui/__snapshots__/handle_update_vulnerability_database_test.snap new file mode 100755 index 00000000000..04810b863cc --- /dev/null +++ b/cmd/grype/cli/ui/__snapshots__/handle_update_vulnerability_database_test.snap @@ -0,0 +1,8 @@ + +[TestHandler_handleUpdateVulnerabilityDatabase/downloading_DB - 1] + ⠋ Vulnerability DB ━━━━━━━━━━━━━━━━━━━━ [current] +--- + +[TestHandler_handleUpdateVulnerabilityDatabase/DB_download_complete - 1] + ✔ Vulnerability DB [current] +--- 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 new file mode 100755 index 00000000000..2d424d73be9 --- /dev/null +++ b/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap @@ -0,0 +1,18 @@ + +[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/task_line - 1] + ⠋ Scanning for vulnerabilities [36 vulnerability matches] +--- + +[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/tree - 1] + ├── 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 [40 vulnerability matches] +--- + +[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/tree - 1] + ├── 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 new file mode 100644 index 00000000000..c8dbecd4a3e --- /dev/null +++ b/cmd/grype/cli/ui/handle_database_diff_started.go @@ -0,0 +1,58 @@ +package ui + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event/monitor" + "github.com/anchore/grype/grype/event/parsers" + "github.com/anchore/grype/internal/log" +) + +type dbDiffProgressStager struct { + monitor *monitor.DBDiff +} + +func (p dbDiffProgressStager) Stage() string { + if progress.IsErrCompleted(p.monitor.StageProgress.Error()) { + return fmt.Sprintf("%d differences found", p.monitor.DifferencesDiscovered.Current()) + } + return p.monitor.Stager.Stage() +} + +func (p dbDiffProgressStager) Current() int64 { + return p.monitor.StageProgress.Current() +} + +func (p dbDiffProgressStager) Error() error { + return p.monitor.StageProgress.Error() +} + +func (p dbDiffProgressStager) Size() int64 { + return p.monitor.StageProgress.Size() +} + +func (m *Handler) handleDatabaseDiffStarted(e partybus.Event) []tea.Model { + mon, err := parsers.ParseDatabaseDiffingStarted(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "Compare Vulnerability DBs", + Running: "Comparing Vulnerability DBs", + Success: "Compared Vulnerability DBs", + }, + taskprogress.WithStagedProgressable(dbDiffProgressStager{monitor: mon}), + ) + + tsk.HideStageOnSuccess = false + + return []tea.Model{tsk} +} diff --git a/cmd/grype/cli/ui/handle_database_diff_started_test.go b/cmd/grype/cli/ui/handle_database_diff_started_test.go new file mode 100644 index 00000000000..b890f791ead --- /dev/null +++ b/cmd/grype/cli/ui/handle_database_diff_started_test.go @@ -0,0 +1,96 @@ +package ui + +import ( + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" +) + +func TestHandler_handleDatabaseDiffStarted(t *testing.T) { + + tests := []struct { + name string + eventFn func(*testing.T) partybus.Event + iterations int + }{ + { + name: "DB diff started", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(50) + + diffs := &progress.Manual{} + diffs.Set(20) + + mon := monitor.DBDiff{ + Stager: &progress.Stage{Current: "current"}, + StageProgress: prog, + DifferencesDiscovered: diffs, + } + + return partybus.Event{ + Type: event.DatabaseDiffingStarted, + Value: mon, + } + }, + }, + { + name: "DB diff complete", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(100) + prog.SetCompleted() + + diffs := &progress.Manual{} + diffs.Set(20) + + mon := monitor.DBDiff{ + Stager: &progress.Stage{Current: "current"}, + StageProgress: prog, + DifferencesDiscovered: diffs, + } + + return partybus.Event{ + Type: event.DatabaseDiffingStarted, + Value: mon, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.eventFn(t) + handler := New(DefaultHandlerConfig()) + handler.WindowSize = tea.WindowSizeMsg{ + Width: 100, + Height: 80, + } + + models := handler.Handle(e) + require.Len(t, models, 1) + model := models[0] + + tsk, ok := model.(taskprogress.Model) + require.True(t, ok) + + got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ + Time: time.Now(), + Sequence: tsk.Sequence(), + ID: tsk.ID(), + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + } +} diff --git a/cmd/grype/cli/ui/handle_update_vulnerability_database.go b/cmd/grype/cli/ui/handle_update_vulnerability_database.go new file mode 100644 index 00000000000..938ae9e1c07 --- /dev/null +++ b/cmd/grype/cli/ui/handle_update_vulnerability_database.go @@ -0,0 +1,53 @@ +package ui + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/dustin/go-humanize" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event/parsers" + "github.com/anchore/grype/internal/log" +) + +type dbDownloadProgressStager struct { + prog progress.StagedProgressable +} + +func (s dbDownloadProgressStager) Stage() string { + stage := s.prog.Stage() + if stage == "downloading" { + // note: since validation is baked into the download progress there is no visibility into this stage. + // for that reason we report "validating" on the last byte being downloaded (which tends to be the longest + // since go-downloader is doing this work). + if s.prog.Current() >= s.prog.Size()-1 { + 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 stage +} + +func (m *Handler) handleUpdateVulnerabilityDatabase(e partybus.Event) []tea.Model { + prog, err := parsers.ParseUpdateVulnerabilityDatabase(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "Vulnerability DB", + }, + taskprogress.WithStagedProgressable(prog), // ignore the static stage provided by the event + taskprogress.WithStager(dbDownloadProgressStager{prog: prog}), + ) + + tsk.HideStageOnSuccess = false + + return []tea.Model{tsk} +} diff --git a/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go b/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go new file mode 100644 index 00000000000..c3b64e1585f --- /dev/null +++ b/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go @@ -0,0 +1,97 @@ +package ui + +import ( + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event" +) + +func TestHandler_handleUpdateVulnerabilityDatabase(t *testing.T) { + + tests := []struct { + name string + eventFn func(*testing.T) partybus.Event + iterations int + }{ + { + name: "downloading DB", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(50) + + mon := struct { + progress.Progressable + progress.Stager + }{ + Progressable: prog, + Stager: &progress.Stage{ + Current: "current", + }, + } + + return partybus.Event{ + Type: event.UpdateVulnerabilityDatabase, + Value: mon, + } + }, + }, + { + name: "DB download complete", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(100) + prog.SetCompleted() + + mon := struct { + progress.Progressable + progress.Stager + }{ + Progressable: prog, + Stager: &progress.Stage{ + Current: "current", + }, + } + + return partybus.Event{ + Type: event.UpdateVulnerabilityDatabase, + Value: mon, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.eventFn(t) + handler := New(DefaultHandlerConfig()) + handler.WindowSize = tea.WindowSizeMsg{ + Width: 100, + Height: 80, + } + + models := handler.Handle(e) + require.Len(t, models, 1) + model := models[0] + + tsk, ok := model.(taskprogress.Model) + require.True(t, ok) + + got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ + Time: time.Now(), + Sequence: tsk.Sequence(), + ID: tsk.ID(), + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + } +} diff --git a/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go b/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go new file mode 100644 index 00000000000..90a157df787 --- /dev/null +++ b/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go @@ -0,0 +1,219 @@ +package ui + +import ( + "fmt" + "sort" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event/monitor" + "github.com/anchore/grype/grype/event/parsers" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" +) + +const ( + branch = "├──" + end = "└──" +) + +var _ progress.StagedProgressable = (*vulnerabilityScanningAdapter)(nil) + +type vulnerabilityProgressTree struct { + mon *monitor.Matching + windowSize tea.WindowSizeMsg + + countBySeverity map[vulnerability.Severity]int64 + unknownCount int64 + fixedCount int64 + ignoredCount int64 + droppedCount int64 + totalCount int64 + severities []vulnerability.Severity + + id uint32 + sequence int + + updateDuration time.Duration + textStyle lipgloss.Style +} + +func newVulnerabilityProgressTree(monitor *monitor.Matching, textStyle lipgloss.Style) vulnerabilityProgressTree { + allSeverities := vulnerability.AllSeverities() + sort.Sort(sort.Reverse(vulnerability.Severities(allSeverities))) + + return vulnerabilityProgressTree{ + mon: monitor, + countBySeverity: make(map[vulnerability.Severity]int64), + severities: allSeverities, + textStyle: textStyle, + } +} + +// vulnerabilityProgressTreeTickMsg indicates that the timer has ticked and we should render a frame. +type vulnerabilityProgressTreeTickMsg struct { + Time time.Time + Sequence int + ID uint32 +} + +type vulnerabilityScanningAdapter struct { + mon *monitor.Matching +} + +func (p vulnerabilityScanningAdapter) Current() int64 { + return p.mon.PackagesProcessed.Current() +} + +func (p vulnerabilityScanningAdapter) Error() error { + return p.mon.MatchesDiscovered.Error() +} + +func (p vulnerabilityScanningAdapter) Size() int64 { + return p.mon.PackagesProcessed.Size() +} + +func (p vulnerabilityScanningAdapter) Stage() string { + return fmt.Sprintf("%d vulnerability matches", p.mon.MatchesDiscovered.Current()-p.mon.Ignored.Current()) +} + +func (m *Handler) handleVulnerabilityScanningStarted(e partybus.Event) []tea.Model { + mon, err := parsers.ParseVulnerabilityScanningStarted(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "Scan for vulnerabilities", + Running: "Scanning for vulnerabilities", + Success: "Scanned for vulnerabilities", + }, + taskprogress.WithStagedProgressable(vulnerabilityScanningAdapter{mon: mon}), + ) + + tsk.HideStageOnSuccess = false + + textStyle := tsk.HintStyle + + return []tea.Model{ + tsk, + newVulnerabilityProgressTree(mon, textStyle), + } +} + +func (l vulnerabilityProgressTree) Init() tea.Cmd { + // this is the periodic update of state information + return func() tea.Msg { + return vulnerabilityProgressTreeTickMsg{ + // The time at which the tick occurred. + Time: time.Now(), + + // The ID of the log frame that this message belongs to. This can be + // helpful when routing messages, however bear in mind that log frames + // will ignore messages that don't contain ID by default. + ID: l.id, + + Sequence: l.sequence, + } + } +} + +func (l vulnerabilityProgressTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + l.windowSize = msg + return l, nil + + 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() + } + + // kick off the next tick + tickCmd := l.handleTick(msg) + + return l, tickCmd + } + + return l, nil +} + +func (l vulnerabilityProgressTree) View() string { + sb := strings.Builder{} + + for idx, sev := range l.severities { + count := l.countBySeverity[sev] + sb.WriteString(fmt.Sprintf("%d %s", count, sev)) + if idx < len(l.severities)-1 { + sb.WriteString(", ") + } + } + if l.unknownCount > 0 { + unknownStr := fmt.Sprintf(" (%d unknown)", l.unknownCount) + sb.WriteString(unknownStr) + } + + status := sb.String() + sb.Reset() + + sevStr := l.textStyle.Render(fmt.Sprintf(" %s by severity: %s", branch, status)) + + sb.WriteString(sevStr) + + 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() +} + +func (l vulnerabilityProgressTree) queueNextTick() tea.Cmd { + return tea.Tick(l.updateDuration, func(t time.Time) tea.Msg { + return vulnerabilityProgressTreeTickMsg{ + Time: t, + ID: l.id, + Sequence: l.sequence, + } + }) +} + +func (l *vulnerabilityProgressTree) handleTick(msg vulnerabilityProgressTreeTickMsg) tea.Cmd { + // If an ID is set, and the ID doesn't belong to this log frame, reject the message. + if msg.ID > 0 && msg.ID != l.id { + return nil + } + + // If a sequence is set, and it's not the one we expect, reject the message. + // This prevents the log frame from receiving too many messages and + // thus updating too frequently. + if msg.Sequence > 0 && msg.Sequence != l.sequence { + return nil + } + + l.sequence++ + + // note: even if the log is completed we should still respond to stage changes and window size events + return l.queueNextTick() +} diff --git a/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go b/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go new file mode 100644 index 00000000000..c3ce11b8fda --- /dev/null +++ b/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go @@ -0,0 +1,165 @@ +package ui + +import ( + "sort" + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" + "github.com/anchore/grype/grype/vulnerability" +) + +func TestHandler_handleVulnerabilityScanningStarted(t *testing.T) { + tests := []struct { + name string + eventFn func(*testing.T) partybus.Event + iterations int + }{ + { + name: "vulnerability scanning in progress", + eventFn: func(t *testing.T) partybus.Event { + return partybus.Event{ + Type: event.VulnerabilityScanningStarted, + Value: getVulnerabilityMonitor(false), + } + }, + }, + { + name: "vulnerability scanning complete", + eventFn: func(t *testing.T) partybus.Event { + return partybus.Event{ + Type: event.VulnerabilityScanningStarted, + Value: getVulnerabilityMonitor(true), + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.eventFn(t) + handler := New(DefaultHandlerConfig()) + handler.WindowSize = tea.WindowSizeMsg{ + Width: 100, + Height: 80, + } + + models := handler.Handle(e) + require.Len(t, models, 2) + + t.Run("task line", func(t *testing.T) { + tsk, ok := models[0].(taskprogress.Model) + require.True(t, ok) + + got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ + Time: time.Now(), + Sequence: tsk.Sequence(), + ID: tsk.ID(), + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + + t.Run("tree", func(t *testing.T) { + log, ok := models[1].(vulnerabilityProgressTree) + require.True(t, ok) + got := runModel(t, log, tt.iterations, vulnerabilityProgressTreeTickMsg{ + Time: time.Now(), + Sequence: log.sequence, + ID: log.id, + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + + }) + } +} + +func getVulnerabilityMonitor(completed bool) monitor.Matching { + pkgs := &progress.Manual{} + pkgs.SetTotal(-1) + if completed { + pkgs.Set(2000) + pkgs.SetCompleted() + } else { + pkgs.Set(300) + } + + vulns := &progress.Manual{} + vulns.SetTotal(-1) + if completed { + vulns.Set(45) + vulns.SetCompleted() + } else { + vulns.Set(40) + } + + fixed := &progress.Manual{} + fixed.SetTotal(-1) + if completed { + fixed.Set(35) + fixed.SetCompleted() + } else { + 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: {}, + vulnerability.MediumSeverity: {}, + vulnerability.LowSeverity: {}, + vulnerability.NegligibleSeverity: {}, + vulnerability.UnknownSeverity: {}, + } + + allSeverities := vulnerability.AllSeverities() + sort.Sort(sort.Reverse(vulnerability.Severities(allSeverities))) + + var count int64 = 1 + for _, sev := range allSeverities { + bySeverityWriter[sev].Add(count) + count++ + } + bySeverityWriter[vulnerability.UnknownSeverity].Add(count) + + bySeverity := map[vulnerability.Severity]progress.Monitorable{} + + for k, v := range bySeverityWriter { + bySeverity[k] = v + } + + return monitor.Matching{ + PackagesProcessed: pkgs, + MatchesDiscovered: vulns, + Fixed: fixed, + Ignored: ignored, + Dropped: dropped, + BySeverity: bySeverity, + } +} diff --git a/cmd/grype/cli/ui/handler.go b/cmd/grype/cli/ui/handler.go new file mode 100644 index 00000000000..4232191862a --- /dev/null +++ b/cmd/grype/cli/ui/handler.go @@ -0,0 +1,66 @@ +package ui + +import ( + "sync" + + tea "github.com/charmbracelet/bubbletea" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/bubbly" + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event" +) + +var _ interface { + bubbly.EventHandler + bubbly.MessageListener + bubbly.HandleWaiter +} = (*Handler)(nil) + +type HandlerConfig struct { + TitleWidth int + AdjustDefaultTask func(taskprogress.Model) taskprogress.Model +} + +type Handler struct { + WindowSize tea.WindowSizeMsg + Running *sync.WaitGroup + Config HandlerConfig + + bubbly.EventHandler +} + +func DefaultHandlerConfig() HandlerConfig { + return HandlerConfig{ + TitleWidth: 30, + } +} + +func New(cfg HandlerConfig) *Handler { + d := bubbly.NewEventDispatcher() + + h := &Handler{ + EventHandler: d, + Running: &sync.WaitGroup{}, + Config: cfg, + } + + // register all supported event types with the respective handler functions + d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{ + event.UpdateVulnerabilityDatabase: h.handleUpdateVulnerabilityDatabase, + event.VulnerabilityScanningStarted: h.handleVulnerabilityScanningStarted, + event.DatabaseDiffingStarted: h.handleDatabaseDiffStarted, + }) + + return h +} + +func (m *Handler) OnMessage(msg tea.Msg) { + if msg, ok := msg.(tea.WindowSizeMsg); ok { + m.WindowSize = msg + } +} + +func (m *Handler) Wait() { + m.Running.Wait() +} diff --git a/cmd/grype/cli/ui/new_task_progress.go b/cmd/grype/cli/ui/new_task_progress.go new file mode 100644 index 00000000000..036f7b37de9 --- /dev/null +++ b/cmd/grype/cli/ui/new_task_progress.go @@ -0,0 +1,19 @@ +package ui + +import "github.com/anchore/bubbly/bubbles/taskprogress" + +func (m Handler) newTaskProgress(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model { + tsk := taskprogress.New(m.Running, opts...) + + tsk.HideProgressOnSuccess = true + tsk.HideStageOnSuccess = true + tsk.WindowSize = m.WindowSize + tsk.TitleWidth = m.Config.TitleWidth + tsk.TitleOptions = title + + if m.Config.AdjustDefaultTask != nil { + tsk = m.Config.AdjustDefaultTask(tsk) + } + + return tsk +} diff --git a/cmd/grype/cli/ui/util_test.go b/cmd/grype/cli/ui/util_test.go new file mode 100644 index 00000000000..81c8525dc71 --- /dev/null +++ b/cmd/grype/cli/ui/util_test.go @@ -0,0 +1,69 @@ +package ui + +import ( + "reflect" + "sync" + "testing" + "unsafe" + + tea "github.com/charmbracelet/bubbletea" +) + +func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, wgs ...*sync.WaitGroup) string { + t.Helper() + if iterations == 0 { + iterations = 1 + } + m.Init() + var cmd tea.Cmd = func() tea.Msg { + return message + } + + for _, wg := range wgs { + if wg != nil { + wg.Wait() + } + } + + for i := 0; cmd != nil && i < iterations; i++ { + msgs := flatten(cmd()) + var nextCmds []tea.Cmd + var next tea.Cmd + for _, msg := range msgs { + t.Logf("Message: %+v %+v\n", reflect.TypeOf(msg), msg) + m, next = m.Update(msg) + nextCmds = append(nextCmds, next) + } + cmd = tea.Batch(nextCmds...) + } + return m.View() +} + +func flatten(p tea.Msg) (msgs []tea.Msg) { + if reflect.TypeOf(p).Name() == "batchMsg" { + partials := extractBatchMessages(p) + for _, m := range partials { + msgs = append(msgs, flatten(m)...) + } + } else { + msgs = []tea.Msg{p} + } + return msgs +} + +func extractBatchMessages(m tea.Msg) (ret []tea.Msg) { + sliceMsgType := reflect.SliceOf(reflect.TypeOf(tea.Cmd(nil))) + value := reflect.ValueOf(m) // note: this is technically unaddressable + + // make our own instance that is addressable + valueCopy := reflect.New(value.Type()).Elem() + valueCopy.Set(value) + + cmds := reflect.NewAt(sliceMsgType, unsafe.Pointer(valueCopy.UnsafeAddr())).Elem() + for i := 0; i < cmds.Len(); i++ { + item := cmds.Index(i) + r := item.Call(nil) + ret = append(ret, r[0].Interface().(tea.Msg)) + } + return ret +} 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 new file mode 100755 index 00000000000..32b9950e43e --- /dev/null +++ b/cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap @@ -0,0 +1,41 @@ + +[Test_postUIEventWriter_write/no_events/stdout - 1] + +--- + +[Test_postUIEventWriter_write/no_events/stderr - 1] + +--- + +[Test_postUIEventWriter_write/all_events/stdout - 1] + + + + + +--- + +[Test_postUIEventWriter_write/all_events/stderr - 1] + + + + + + + +A newer version of grype is available for download: v0.33.0 (installed version is [not provided]) + +--- + +[Test_postUIEventWriter_write/quiet_only_shows_report/stdout - 1] + + +--- + +[Test_postUIEventWriter_write/quiet_only_shows_report/stderr - 1] + +--- diff --git a/cmd/grype/internal/ui/no_ui.go b/cmd/grype/internal/ui/no_ui.go new file mode 100644 index 00000000000..a05b2f49de7 --- /dev/null +++ b/cmd/grype/internal/ui/no_ui.go @@ -0,0 +1,42 @@ +package ui + +import ( + "os" + + "github.com/wagoodman/go-partybus" + + "github.com/anchore/clio" + "github.com/anchore/grype/grype/event" +) + +var _ clio.UI = (*NoUI)(nil) + +type NoUI struct { + finalizeEvents []partybus.Event + subscription partybus.Unsubscribable + quiet bool +} + +func None(quiet bool) *NoUI { + return &NoUI{ + quiet: quiet, + } +} + +func (n *NoUI) Setup(subscription partybus.Unsubscribable) error { + n.subscription = subscription + return nil +} + +func (n *NoUI) Handle(e partybus.Event) error { + switch e.Type { + 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) + } + return nil +} + +func (n NoUI) Teardown(_ bool) error { + return newPostUIEventWriter(os.Stdout, os.Stderr).write(n.quiet, n.finalizeEvents...) +} diff --git a/cmd/grype/internal/ui/post_ui_event_writer.go b/cmd/grype/internal/ui/post_ui_event_writer.go new file mode 100644 index 00000000000..f6949daa846 --- /dev/null +++ b/cmd/grype/internal/ui/post_ui_event_writer.go @@ -0,0 +1,139 @@ +package ui + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/hashicorp/go-multierror" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/parsers" + "github.com/anchore/grype/internal/log" +) + +type postUIEventWriter struct { + handles []postUIHandle +} + +type postUIHandle struct { + respectQuiet bool + event partybus.EventType + writer io.Writer + dispatch eventWriter +} + +type eventWriter func(io.Writer, ...partybus.Event) error + +func newPostUIEventWriter(stdout, stderr io.Writer) *postUIEventWriter { + return &postUIEventWriter{ + handles: []postUIHandle{ + { + event: event.CLIReport, + respectQuiet: false, + writer: stdout, + dispatch: writeReports, + }, + { + event: event.CLINotification, + respectQuiet: true, + writer: stderr, + dispatch: writeNotifications, + }, + { + event: event.CLIAppUpdateAvailable, + respectQuiet: true, + writer: stderr, + dispatch: writeAppUpdate, + }, + }, + } +} + +func (w postUIEventWriter) write(quiet bool, events ...partybus.Event) error { + var errs error + for _, h := range w.handles { + if quiet && h.respectQuiet { + continue + } + + for _, e := range events { + if e.Type != h.event { + continue + } + + if err := h.dispatch(h.writer, e); err != nil { + errs = multierror.Append(errs, err) + } + } + } + return errs +} + +func writeReports(writer io.Writer, events ...partybus.Event) error { + var reports []string + for _, e := range events { + _, report, err := parsers.ParseCLIReport(e) + if err != nil { + log.WithFields("error", err).Warn("failed to gather final report") + continue + } + + // remove all whitespace padding from the end of the report + reports = append(reports, strings.TrimRight(report, "\n ")+"\n") + } + + // prevent the double new-line at the end of the report + report := strings.Join(reports, "\n") + + if _, err := fmt.Fprint(writer, report); err != nil { + return fmt.Errorf("failed to write final report to stdout: %w", err) + } + return nil +} + +func writeNotifications(writer io.Writer, events ...partybus.Event) error { + // 13 = high intensity magenta (ANSI 16 bit code) + style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")) + + for _, e := range events { + _, notification, err := parsers.ParseCLINotification(e) + if err != nil { + log.WithFields("error", err).Warn("failed to parse notification") + continue + } + + if _, err := fmt.Fprintln(writer, style.Render(notification)); err != nil { + // don't let this be fatal + log.WithFields("error", err).Warn("failed to write final notifications") + } + } + return nil +} + +func writeAppUpdate(writer io.Writer, events ...partybus.Event) error { + // 13 = high intensity magenta (ANSI 16 bit code) + italics + style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true) + + for _, e := range events { + 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") + } + } + return nil +} diff --git a/cmd/grype/internal/ui/post_ui_event_writer_test.go b/cmd/grype/internal/ui/post_ui_event_writer_test.go new file mode 100644 index 00000000000..329ce2b71f8 --- /dev/null +++ b/cmd/grype/internal/ui/post_ui_event_writer_test.go @@ -0,0 +1,102 @@ +package ui + +import ( + "bytes" + "testing" + + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "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) { + + tests := []struct { + name string + quiet bool + events []partybus.Event + wantErr require.ErrorAssertionFunc + }{ + { + name: "no events", + }, + { + name: "all events", + events: []partybus.Event{ + { + Type: event.CLINotification, + Value: "\n\n\n\n", + }, + { + Type: event.CLINotification, + Value: "", + }, + { + Type: event.CLIAppUpdateAvailable, + Value: parsers.UpdateCheck{ + New: "v0.33.0", + Current: "[not provided]", + }, + }, + { + Type: event.CLINotification, + Value: "", + }, + { + Type: event.CLIReport, + Value: "\n\n\n\n", + }, + { + Type: event.CLIReport, + Value: "", + }, + }, + }, + { + name: "quiet only shows report", + quiet: true, + events: []partybus.Event{ + + { + Type: event.CLINotification, + Value: "", + }, + { + Type: event.CLIAppUpdateAvailable, + Value: parsers.UpdateCheck{ + New: "", + Current: "", + }, + }, + { + Type: event.CLIReport, + Value: "", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + w := newPostUIEventWriter(stdout, stderr) + + tt.wantErr(t, w.write(tt.quiet, tt.events...)) + + t.Run("stdout", func(t *testing.T) { + snaps.MatchSnapshot(t, stdout.String()) + }) + + t.Run("stderr", func(t *testing.T) { + snaps.MatchSnapshot(t, stderr.String()) + }) + }) + } +} diff --git a/cmd/grype/internal/ui/ui.go b/cmd/grype/internal/ui/ui.go new file mode 100644 index 00000000000..bd529023198 --- /dev/null +++ b/cmd/grype/internal/ui/ui.go @@ -0,0 +1,187 @@ +package ui + +import ( + "fmt" + "os" + "sync" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/bubbly" + "github.com/anchore/bubbly/bubbles/frame" + "github.com/anchore/clio" + "github.com/anchore/go-logger" + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/internal/bus" + "github.com/anchore/grype/internal/log" +) + +var _ interface { + tea.Model + partybus.Responder + clio.UI +} = (*UI)(nil) + +type UI struct { + program *tea.Program + running *sync.WaitGroup + quiet bool + subscription partybus.Unsubscribable + finalizeEvents []partybus.Event + + handler *bubbly.HandlerCollection + frame tea.Model +} + +func New(quiet bool, handlers ...bubbly.EventHandler) *UI { + return &UI{ + handler: bubbly.NewHandlerCollection(handlers...), + frame: frame.New(), + running: &sync.WaitGroup{}, + quiet: quiet, + } +} + +func (m *UI) Setup(subscription partybus.Unsubscribable) error { + // we still want to collect log messages, however, we also the logger shouldn't write to the screen directly + if logWrapper, ok := log.Get().(logger.Controller); ok { + logWrapper.SetOutput(m.frame.(*frame.Frame).Footer()) + } + + m.subscription = subscription + m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin)) + m.running.Add(1) + + go func() { + defer m.running.Done() + if _, err := m.program.Run(); err != nil { + log.Errorf("unable to start UI: %+v", err) + bus.ExitWithInterrupt() + } + }() + + return nil +} + +func (m *UI) Handle(e partybus.Event) error { + if m.program != nil { + m.program.Send(e) + } + return nil +} + +func (m *UI) Teardown(force bool) error { + 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 { + _ = 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() + + _ = 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) + + return newPostUIEventWriter(os.Stdout, os.Stderr).write(m.quiet, m.finalizeEvents...) +} + +// bubbletea.Model functions + +func (m UI) Init() tea.Cmd { + return m.frame.Init() +} + +func (m UI) RespondsTo() []partybus.EventType { + return append([]partybus.EventType{ + event.CLIReport, + event.CLINotification, + event.CLIAppUpdateAvailable, + }, m.handler.RespondsTo()...) +} + +func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // note: we need a pointer receiver such that the same instance of UI used in Teardown is referenced (to keep finalize events) + + var cmds []tea.Cmd + + // allow for non-partybus UI updates (such as window size events). Note: these must not affect existing models, + // that is the responsibility of the frame object on this UI object. The handler is a factory of models + // which the frame is responsible for the lifecycle of. This update allows for injecting the initial state + // of the world when creating those models. + m.handler.OnMessage(msg) + + 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": + bus.ExitWithInterrupt() + return m, tea.Quit + } + + case partybus.Event: + log.WithFields("component", "ui").Tracef("event: %q", msg.Type) + + switch msg.Type { + 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 event loop call Teardown() which will explicitly wait for these components + return m, nil + } + + for _, newModel := range m.handler.Handle(msg) { + if newModel == nil { + continue + } + cmds = append(cmds, newModel.Init()) + m.frame.(*frame.Frame).AppendModel(newModel) + } + // intentionally fallthrough to update the frame model + } + + frameModel, cmd := m.frame.Update(msg) + m.frame = frameModel + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +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 new file mode 100644 index 00000000000..f3f11363fdd --- /dev/null +++ b/cmd/grype/main.go @@ -0,0 +1,34 @@ +package main + +import ( + _ "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() { + app := cli.Application( + clio.Identification{ + Name: applicationName, + Version: version, + BuildDate: buildDate, + GitCommit: gitCommit, + GitDescription: gitDescription, + }, + ) + + app.Run() +} diff --git a/cmd/report_writer.go b/cmd/report_writer.go deleted file mode 100644 index 99cdbfcde00..00000000000 --- a/cmd/report_writer.go +++ /dev/null @@ -1,33 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - "strings" -) - -func reportWriter() (io.Writer, func() error, error) { - nop := func() error { return nil } - path := strings.TrimSpace(appConfig.File) - - switch len(path) { - case 0: - return os.Stdout, nop, nil - - default: - reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - - if err != nil { - return nil, nop, fmt.Errorf("unable to create report file: %w", err) - } - - return reportFile, func() error { - if !appConfig.Quiet { - fmt.Printf("Report written to %q\n", path) - } - - return reportFile.Close() - }, nil - } -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index c93dba75d60..00000000000 --- a/cmd/root.go +++ /dev/null @@ -1,520 +0,0 @@ -package cmd - -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/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" - "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/ui" - "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: format.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.StringP( - "output", "o", "", - fmt.Sprintf("report output formatter, formats=%v, deprecated formats=%v", presenter.AvailableFormats, presenter.DeprecatedFormats), - ) - - flags.StringP( - "file", "", "", - "file to write the 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] - } - - reporter, closer, err := reportWriter() - defer func() { - if err := closer(); err != nil { - log.Warnf("unable to write to report destination: %+v", err) - } - }() - - if err != nil { - return err - } - - return eventLoop( - startWorker(userInput, appConfig.FailOnSeverity), - setupSignals(), - eventSubscription, - stereoscope.Cleanup, - ui.Select(isVerbose(), appConfig.Quiet, reporter)..., - ) -} - -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) - - presenterConfig, err := presenter.ValidatedConfig(appConfig.Output, appConfig.OutputTemplateFile, 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.Pacakge, 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 - } - } - - pb := models.PresenterConfig{ - Matches: *remainingMatches, - IgnoredMatches: ignoredMatches, - Packages: packages, - Context: pkgContext, - MetadataProvider: str, - SBOM: sbom, - AppConfig: appConfig, - DBStatus: status, - } - - bus.Publish(partybus.Event{ - Type: event.VulnerabilityScanningFinished, - Value: presenter.GetPresenter(presenterConfig, pb), - }) - }() - 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.AppUpdateAvailable, - 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/signals.go b/cmd/signals.go deleted file mode 100644 index f20379d1eeb..00000000000 --- a/cmd/signals.go +++ /dev/null @@ -1,20 +0,0 @@ -package cmd - -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/util.go b/cmd/util.go deleted file mode 100644 index e9fa6bc3ff8..00000000000 --- a/cmd/util.go +++ /dev/null @@ -1,15 +0,0 @@ -package cmd - -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/version.go b/cmd/version.go deleted file mode 100644 index fcdd3379b3a..00000000000 --- a/cmd/version.go +++ /dev/null @@ -1,64 +0,0 @@ -package cmd - -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/go.mod b/go.mod index 58a101adaa5..5c693ebbf97 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,38 @@ module github.com/anchore/grype -go 1.19 +go 1.21.1 require ( - github.com/CycloneDX/cyclonedx-go v0.7.1 + github.com/CycloneDX/cyclonedx-go v0.7.2 github.com/Masterminds/sprig/v3 v3.2.3 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/adrg/xdg v0.4.0 + github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 + github.com/anchore/clio v0.0.0-20231016125544-c98a83e1c7fc + github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a 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-20230522170632-e14bc4437b2e + github.com/anchore/stereoscope v0.0.0-20230925132944-bf05af58eb44 + github.com/anchore/syft v0.93.0 + github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 github.com/bmatcuk/doublestar/v2 v2.0.4 - github.com/docker/docker v24.0.1+incompatible + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.9.1 + github.com/docker/docker v24.0.6+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/gabriel-vasile/mimetype v1.4.3 + github.com/gkampitakis/go-snaps v0.4.11 + github.com/glebarez/sqlite v1.9.0 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/google/go-cmp v0.6.0 + github.com/google/go-containerregistry v0.16.1 + github.com/google/uuid v1.3.1 + 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.3 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f @@ -29,171 +40,214 @@ require ( 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/mitchellh/mapstructure v1.5.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/pkg/profile v1.7.0 // indirect // 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.2 - github.com/spf13/afero v1.9.5 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/afero v1.10.0 github.com/spf13/cobra v1.7.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.15.0 - github.com/stretchr/testify v1.8.3 - github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 - github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 - github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.16.0 // indirect + github.com/stretchr/testify v1.8.4 + github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 + github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b + github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 github.com/x-cray/logrus-prefixed-formatter v0.5.2 - golang.org/x/term v0.8.0 - gopkg.in/yaml.v2 v2.4.0 - gorm.io/gorm v1.23.10 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 + golang.org/x/term v0.13.0 // indirect + gorm.io/gorm v1.25.5 + modernc.org/sqlite v1.26.0 // indirect ) -require ( - github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 - github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 - github.com/anchore/syft v0.82.0 - github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b - github.com/mitchellh/mapstructure v1.5.0 -) +require github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 require ( - cloud.google.com/go v0.110.0 // indirect - cloud.google.com/go/compute v1.19.1 // indirect + cloud.google.com/go v0.110.2 // indirect + cloud.google.com/go/compute v1.20.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 + cloud.google.com/go/iam v1.1.0 // indirect + cloud.google.com/go/storage v1.29.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect + github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 // indirect github.com/DataDog/zstd v1.4.5 // 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/Masterminds/semver/v3 v3.2.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect + github.com/Microsoft/hcsshim v0.10.0-rc.7 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/acobaugh/osrelease v0.1.0 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe // indirect github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // 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/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect + github.com/aws/aws-sdk-go v1.44.288 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/charmbracelet/harmonica v0.2.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/containerd/containerd v1.7.0 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/containerd/ttrpc v1.2.1 // indirect + github.com/containerd/typeurl/v2 v2.1.0 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // 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/distribution/reference v0.5.0 // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/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/go-git/gcfg v1.5.0 // indirect - github.com/go-git/go-billy/v5 v5.4.1 // indirect - github.com/go-git/go-git/v5 v5.6.1 // indirect + github.com/github/go-spdx/v2 v2.2.0 // indirect + github.com/gkampitakis/ciinfo v0.2.5 // indirect + github.com/gkampitakis/go-diff v1.3.2 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.9.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-restruct/restruct v1.2.0-alpha // 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/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/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.7.1 // indirect + github.com/google/s2a-go v0.1.4 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect + github.com/googleapis/gax-go/v2 v2.11.0 // 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.13 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/imdario/mergo v0.3.15 // 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/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/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/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/signal v0.7.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // 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/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect + github.com/opencontainers/runc v1.1.5 // indirect + github.com/opencontainers/runtime-spec v1.1.0-rc.1 // 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.6 // 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/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // 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.11.0 // indirect + github.com/saferwall/pe v1.4.7 // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/sassoftware/go-rpmutils v0.2.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/skeema/knownhosts v1.1.0 // indirect - github.com/spdx/tools-golang v0.5.0 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/skeema/knownhosts v1.2.0 // indirect + github.com/spdx/tools-golang v0.5.3 // 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/sif/v2 v2.11.5 // indirect github.com/sylabs/squashfs v0.6.1 // indirect github.com/therootcompany/xz v1.0.1 // indirect + github.com/tidwall/gjson v1.16.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/vifraa/gopom v1.0.0 // 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/zclconf/go-cty v1.14.0 // indirect + github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 // indirect + go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.14.0 // indirect + go.opentelemetry.io/otel/trace v1.14.0 // indirect go.uber.org/goleak v1.2.0 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b // indirect - golang.org/x/mod v0.10.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.2.0 // indirect - golang.org/x/tools v0.8.0 // indirect + golang.org/x/tools v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/api v0.114.0 // indirect + google.golang.org/api v0.128.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd // indirect - google.golang.org/grpc v1.54.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/grpc v1.56.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // 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/libc v1.24.1 // 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.22.1 // indirect - modernc.org/strutil v1.1.3 // indirect - modernc.org/token v1.1.0 // indirect + modernc.org/memory v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 600f5137d9c..0b3d5d53227 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9 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.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= +cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= 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/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= @@ -71,8 +71,8 @@ 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.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= @@ -113,13 +113,12 @@ cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y97 cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= 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.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.1.0 h1:67gSqaPukx7O8WLLHMa0PNs3EBGd2eE4d+psbO/CO94= +cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= 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/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/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= @@ -177,8 +176,8 @@ 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 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= 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/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= @@ -190,14 +189,21 @@ cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xX cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= 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= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 h1:+vTEFqeoeur6XSq06bs+roX3YiT49gUniJK7Zky7Xjg= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/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/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.7.2 h1:kKQ0t1dPOlugSIYVOMiMtFqeXI2wp/f5DBIdfux8gnQ= +github.com/CycloneDX/cyclonedx-go v0.7.2/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk= 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= @@ -205,16 +211,19 @@ github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJ 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/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/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/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/hcsshim v0.10.0-rc.7 h1:HBytQPxcv8Oy4244zbQbe6hnOnx544eL5QPUqhJldz8= +github.com/Microsoft/hcsshim v0.10.0-rc.7/go.mod h1:ILuwjA+kNW+MrN/w5un7n3mTqkwsFu4Bp05/okFUZlE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 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= @@ -227,8 +236,14 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy 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/go-logger v0.0.0-20220728155337-03b66a5207d8 h1:imgMA0gN0TZx7PSa/pdWqXadBvrz8WsN6zySzCe4XX0= -github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8/go.mod h1:+gPap4jha079qzRTUaehv+UZ6sSdaNwkH0D3b6zhTuk= +github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 h1:xGu4/uMWucwWV0YV3fpFIQZ6KVfS/Wfhmma8t0s0vRo= +github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461/go.mod h1:Ger02eh5NpPm2IqkPAy396HU1KlK3BhOeCljDYXySSk= +github.com/anchore/clio v0.0.0-20231016125544-c98a83e1c7fc h1:A1KFO+zZZmbNlz1+WKsCF0RKVx6XRoxsAG3lrqH9hUQ= +github.com/anchore/clio v0.0.0-20231016125544-c98a83e1c7fc/go.mod h1:QeWvNzxsrUNxcs6haQo3OtISfXUXW0qAuiG4EQiz0GU= +github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe h1:pVpLCGWdNeskAw7vGNdCAcGMezrNljHIqOc9HaOja5M= +github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe/go.mod h1:82EGoxZTfBXSW0/zollEP+Qs3wkiKmip5yBT5j+eZpY= +github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw= +github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= github.com/anchore/go-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-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= @@ -239,12 +254,10 @@ github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE 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-20230522170632-e14bc4437b2e h1:YPWJxds1hKRedS92u7O6D6ULVOx1F2HGgS4CWqJdBYw= -github.com/anchore/stereoscope v0.0.0-20230522170632-e14bc4437b2e/go.mod h1:0LsgHgXO4QFnk2hsYwtqd3fR18PIZXlFLIl2qb9tu3g= -github.com/anchore/syft v0.82.0 h1:s8uHBCB3Q3d0Q8yTg7zRklLQnuYfveCdveNhwlOPwT8= -github.com/anchore/syft v0.82.0/go.mod h1:J1LbCDkCqu16DkG5CkFq1SQaMkQ+7ucsJZD9SjDmXzs= +github.com/anchore/stereoscope v0.0.0-20230925132944-bf05af58eb44 h1:dKMvcpgqsRrX1ZWyqG53faVW+BahlaAO1RUEc7/rOjA= +github.com/anchore/stereoscope v0.0.0-20230925132944-bf05af58eb44/go.mod h1:RtbeDCho0pxkPqrB1QNf/Jlxfc9juLmtYZAf2UbpJfk= +github.com/anchore/syft v0.93.0 h1:0b4+4Ob6Mmbudp4Gid6JZh7402nQ3sSD5PMi5dFOpDY= +github.com/anchore/syft v0.93.0/go.mod h1:RuSzHMGKBoiJkeR859moBeOTNnfPref3AloEMSYKDL8= 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= @@ -252,7 +265,12 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG 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/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.0-20210121094942-22b2f8951d46 h1:vmXNl+HDfqqXgr0uY1UgK1GAhps8nbAAtqHNBcgyf+4= +github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46/go.mod h1:olhPNdiiAAMiSujemd1O/sc6GcyePr23f/6uGKtthNg= +github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 h1:rcEG5HI490FF0a7zuvxOxen52ddygCfNVjP0XOCMl+M= +github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= 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= @@ -261,8 +279,10 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI 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/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/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= @@ -276,20 +296,30 @@ github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQm 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/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/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 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/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/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.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= 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/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -302,39 +332,64 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH 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/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +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/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= +github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= 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/containerd/ttrpc v1.2.1 h1:VWv/Rzx023TBLv4WQ+9WPXlBG/s3rsRjY3i9AJ2BJdE= +github.com/containerd/ttrpc v1.2.1/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= +github.com/containerd/typeurl/v2 v2.1.0 h1:yNAhJvbNEANt7ck48IlEGOxP7YAp6LLpGn5jZACDNIE= +github.com/containerd/typeurl/v2 v2.1.0/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.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/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.1+incompatible h1:NxN81beIxDlUaVt46iUQrYHD9/W3u9EGl52r86O/IGw= -github.com/docker/docker v24.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+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 v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= +github.com/docker/docker v24.0.6+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/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.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 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= @@ -359,29 +414,39 @@ 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.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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/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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 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/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= +github.com/github/go-spdx/v2 v2.2.0 h1:yBBLMasHA70Ujd35OpL/OjJOWWVNXcJGbars0GinGRI= +github.com/github/go-spdx/v2 v2.2.0/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w= +github.com/gkampitakis/ciinfo v0.2.5 h1:K0mac90lGguc1conc46l0YEsB7/nioWCqSnJp/6z8Eo= +github.com/gkampitakis/ciinfo v0.2.5/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.11 h1:7qKaozbTQEvHeG0bt6osdjdTDTnWYdIrLx43a7DEDu4= +github.com/gkampitakis/go-snaps v0.4.11/go.mod h1:N4TpqxI4CqKUfHzDFqrqZ5UP0I0ESz2g2NMslh7MiJw= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs= +github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= -github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= -github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= -github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -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.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ= -github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= -github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= -github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= +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.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= +github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY= +github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0= 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= @@ -389,6 +454,11 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 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.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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-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= @@ -397,6 +467,7 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/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= @@ -454,10 +525,11 @@ 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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ= +github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= 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= @@ -467,6 +539,7 @@ github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIG 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/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= @@ -486,15 +559,18 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8I 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/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.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.4 h1:uGy6JWR/uMIILU8wbf+OkstIrNiMjGpEIyhx8f6W7s4= +github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= 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= @@ -504,13 +580,13 @@ 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.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= +github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= 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/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 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= @@ -523,8 +599,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.3 h1:bN2+Fw9XPFvOCjB0UOevFIMICZ7G2XSQHzfvLUyOM5E= +github.com/hashicorp/go-getter v1.7.3/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= 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= @@ -560,23 +636,23 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO 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/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.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +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/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/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/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -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= @@ -590,9 +666,8 @@ 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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +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/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= @@ -618,13 +693,16 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB 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 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +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/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/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/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -640,7 +718,6 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 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= @@ -649,12 +726,13 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k 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-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.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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= @@ -684,16 +762,34 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR 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/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +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/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= +github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +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/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +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/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.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 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= @@ -707,26 +803,41 @@ 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.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/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/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0-rc.1 h1:wHa9jroFfKGQqFHj0I1fMRKLl0pfj+ynAqBxo3v6u9w= +github.com/opencontainers/runtime-spec v1.1.0-rc.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +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/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/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= +github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= 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.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= -github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +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/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/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= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -751,49 +862,66 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/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/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.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/saferwall/pe v1.4.7 h1:A+G3DxX49paJ5OsxBfHKskhyDtmTjShlDmBd81IsHlQ= +github.com/saferwall/pe v1.4.7/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +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.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= 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/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/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.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= 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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= -github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= -github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag= +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.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= +github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= -github.com/spdx/tools-golang v0.5.0 h1:/fqihV2Jna7fmow65dHpgKNsilgLK7ICpd2tkCnPEyY= -github.com/spdx/tools-golang v0.5.0/go.mod h1:kkGlrSXXfHwuSzHQZJRV3aKu9ZXCq/MSf2+xyiJH1lM= +github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY= +github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI= 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/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +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/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= @@ -802,8 +930,8 @@ github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0 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/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= -github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= 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= @@ -818,42 +946,65 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 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/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/sif/v2 v2.11.5 h1:7ssPH3epSonsTrzbS1YxeJ9KuqAN7ISlSM61a7j/mQM= +github.com/sylabs/sif/v2 v2.11.5/go.mod h1:GBoZs9LU3e4yJH1dcZ3Akf/jsqYgy5SeguJQC+zd75Y= github.com/sylabs/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ= github.com/sylabs/squashfs v0.6.1/go.mod h1:ZwpbPCj0ocIvMy2br6KZmix6Gzh6fsGQcCnydMF+Kx8= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= +github.com/terminalstatic/go-xsd-validate v0.1.5/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.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg= +github.com/tidwall/gjson v1.16.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= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +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.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 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/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8= -github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw= -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/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb h1:Yz6VVOcLuWLAHYlJzTw7JKnWxdV/WXpug2X0quEzRnY= -github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb/go.mod h1:nDi3BAC5nEbVbg+WSJDHLbjHv0ZToq8nMPA97XMxF3E= +github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0= +github.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +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-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8= +github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/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/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= @@ -864,11 +1015,15 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de 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.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/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= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -878,6 +1033,10 @@ 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/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= +go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= +go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= @@ -885,10 +1044,8 @@ 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/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 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-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -900,14 +1057,14 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP 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-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/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= @@ -918,8 +1075,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 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-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -947,9 +1104,9 @@ 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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= @@ -1004,15 +1161,14 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/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.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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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= @@ -1039,8 +1195,8 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/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.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= 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= @@ -1055,8 +1211,9 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ 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-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 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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= @@ -1071,6 +1228,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/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= @@ -1080,6 +1238,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w 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-20191115151921-52ab43148777/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= @@ -1125,10 +1284,12 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/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-20210906170528-6f6e22806c34/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-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/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= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1137,7 +1298,6 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc 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-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= @@ -1149,7 +1309,6 @@ 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-20220825204002-c680a09ffe64/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1157,18 +1316,17 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 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= @@ -1178,10 +1336,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.8.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 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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= @@ -1232,7 +1393,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -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= @@ -1246,9 +1406,9 @@ 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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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= @@ -1308,8 +1468,8 @@ google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ 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.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +google.golang.org/api v0.128.0 h1:RjPESny5CnQRn9V6siglged+DZCgfu9l6mO9dkX9VOg= +google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= 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= @@ -1425,8 +1585,12 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw 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-20230403163135-c38d8f061ccd h1:sLpv7bNL1AsX3fdnWh9WVh7ejIzXdOc1RRHGeAmeStU= -google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= 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= @@ -1463,8 +1627,8 @@ 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.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE= +google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= 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= @@ -1481,13 +1645,12 @@ 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.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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= @@ -1511,13 +1674,12 @@ 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.0/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.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 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= @@ -1525,54 +1687,15 @@ 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= -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= -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/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/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= -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/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= +modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -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/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.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= -modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= -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/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= +modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= +modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw= +modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= 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= diff --git a/grype/db/curator.go b/grype/db/curator.go index 57711aa0c4c..da0b21da1dd 100644 --- a/grype/db/curator.go +++ b/grype/db/curator.go @@ -119,9 +119,7 @@ func (c *Curator) Delete() error { func (c *Curator) Update() (bool, error) { // 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) @@ -171,7 +169,7 @@ func (c *Curator) Update() (bool, error) { return true, nil } - stage.Current = "no update available" + stage.Set("no update available") return false, nil } @@ -207,26 +205,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() diff --git a/grype/db/curator_test.go b/grype/db/curator_test.go index acb20216bf3..bb3ee22ac09 100644 --- a/grype/db/curator_test.go +++ b/grype/db/curator_test.go @@ -21,14 +21,14 @@ import ( "github.com/stretchr/testify/require" "github.com/wagoodman/go-progress" - "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/file" + "github.com/anchore/grype/internal/stringutil" ) type testGetter struct { file map[string]string dir map[string]string - calls internal.StringSet + calls stringutil.StringSet fs afero.Fs } @@ -36,7 +36,7 @@ func newTestGetter(fs afero.Fs, f, d map[string]string) *testGetter { return &testGetter{ file: f, dir: d, - calls: internal.NewStringSet(), + calls: stringutil.NewStringSet(), fs: fs, } } diff --git a/grype/db/internal/gormadapter/open.go b/grype/db/internal/gormadapter/open.go index 00ad3eefeb7..6448dada25a 100644 --- a/grype/db/internal/gormadapter/open.go +++ b/grype/db/internal/gormadapter/open.go @@ -4,9 +4,8 @@ import ( "fmt" "os" + "github.com/glebarez/sqlite" "gorm.io/gorm" - - "github.com/anchore/sqlite" ) var writerStatements = []string{ diff --git a/grype/db/test-fixtures/tls/Makefile b/grype/db/test-fixtures/tls/Makefile index f0ca3949475..be149c85410 100644 --- a/grype/db/test-fixtures/tls/Makefile +++ b/grype/db/test-fixtures/tls/Makefile @@ -9,14 +9,14 @@ serve: www/listing.json www/db.tar.gz server.crt grype-test-fail: clean-dbdir dbdir GRYPE_DB_CACHE_DIR=$(shell pwd)/dbdir \ GRYPE_DB_UPDATE_URL=https://$(shell hostname).local/listing.json \ - go run ../../../../main.go -vv alpine:latest + go run ../../../../cmd/grype -vv alpine:latest .PHONY: grype-test-pass grype-test-pass: clean-dbdir dbdir GRYPE_DB_CA_CERT=$(shell pwd)/server.crt \ GRYPE_DB_CACHE_DIR=$(shell pwd)/dbdir \ GRYPE_DB_UPDATE_URL=https://$(shell hostname).local/listing.json \ - go run ../../../../main.go -vv alpine:latest + go run ../../../../cmd/grype -vv alpine:latest dbdir: mkdir -p dbdir diff --git a/grype/db/v1/store/store.go b/grype/db/v1/store/store.go index d6ffab4816d..9b22bc89331 100644 --- a/grype/db/v1/store/store.go +++ b/grype/db/v1/store/store.go @@ -4,14 +4,14 @@ import ( "fmt" "sort" + _ "github.com/glebarez/sqlite" // provide the sqlite dialect to gorm via import "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" - _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import + "github.com/anchore/grype/internal/stringutil" ) // store holds an instance of the database connection @@ -172,7 +172,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v1.VulnerabilityMetadata) e existing.CvssV3 = m.CvssV3 } - links := internal.NewStringSetFromSlice(existing.Links) + links := stringutil.NewStringSetFromSlice(existing.Links) for _, l := range m.Links { links.Add(l) } diff --git a/grype/db/v2/store/store.go b/grype/db/v2/store/store.go index 073cbaf01ff..ee5a313d296 100644 --- a/grype/db/v2/store/store.go +++ b/grype/db/v2/store/store.go @@ -4,14 +4,14 @@ import ( "fmt" "sort" + _ "github.com/glebarez/sqlite" // provide the sqlite dialect to gorm via import "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" - _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import + "github.com/anchore/grype/internal/stringutil" ) // store holds an instance of the database connection @@ -171,7 +171,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v2.VulnerabilityMetadata) e existing.CvssV3 = m.CvssV3 } - links := internal.NewStringSetFromSlice(existing.Links) + links := stringutil.NewStringSetFromSlice(existing.Links) for _, l := range m.Links { links.Add(l) } diff --git a/grype/db/v3/namespace.go b/grype/db/v3/namespace.go index 386c89c793c..ab43539ac64 100644 --- a/grype/db/v3/namespace.go +++ b/grype/db/v3/namespace.go @@ -6,8 +6,8 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" "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" ) @@ -110,7 +110,7 @@ func defaultPackageNamer(p pkg.Package) []string { } func githubJavaPackageNamer(p pkg.Package) []string { - names := internal.NewStringSet() + names := stringutil.NewStringSet() // all github advisories are stored by ":" if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok { diff --git a/grype/db/v3/store/diff.go b/grype/db/v3/store/diff.go index 24a473a46cc..ff32486818b 100644 --- a/grype/db/v3/store/diff.go +++ b/grype/db/v3/store/diff.go @@ -5,8 +5,8 @@ import ( "github.com/wagoodman/go-progress" v3 "github.com/anchore/grype/grype/db/v3" - diffEvents "github.com/anchore/grype/grype/differ/events" "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/internal/bus" ) @@ -32,18 +32,21 @@ type storeMetadata struct { } // create manual progress bars for tracking the database diff's progress -func trackDiff() (*progress.Manual, *progress.Manual) { - rowsProcessed := progress.Manual{} - differencesDiscovered := progress.Manual{} +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: diffEvents.Monitor{ - RowsProcessed: progress.Monitorable(&rowsProcessed), - DifferencesDiscovered: progress.Monitorable(&differencesDiscovered), + Value: monitor.DBDiff{ + Stager: stager, + StageProgress: progress.Progressable(stageProgress), + DifferencesDiscovered: progress.Monitorable(differencesDiscovered), }, }) - return &rowsProcessed, &differencesDiscovered + return stageProgress, differencesDiscovered, stager } // creates a map from an unpackaged key to a list of all packages associated with it diff --git a/grype/db/v3/store/store.go b/grype/db/v3/store/store.go index 5dce10e5647..8643ac4162c 100644 --- a/grype/db/v3/store/store.go +++ b/grype/db/v3/store/store.go @@ -4,14 +4,14 @@ import ( "fmt" "sort" + _ "github.com/glebarez/sqlite" // provide the sqlite dialect to gorm via import "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" - _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import + "github.com/anchore/grype/internal/stringutil" ) // store holds an instance of the database connection @@ -179,7 +179,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v3.VulnerabilityMetadata) e existing.Cvss = append(existing.Cvss, incomingCvss) } - links := internal.NewStringSetFromSlice(existing.URLs) + links := stringutil.NewStringSetFromSlice(existing.URLs) for _, l := range m.URLs { links.Add(l) } @@ -249,37 +249,45 @@ func (s *store) GetAllVulnerabilityMetadata() (*[]v3.VulnerabilityMetadata, erro // DiffStore creates a diff between the current sql database and the given store func (s *store) DiffStore(targetStore v3.StoreReader) (*[]v3.Diff, error) { - rowsProgress, diffItems := trackDiff() + // 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 diff --git a/grype/db/v4/pkg/resolver/java/resolver.go b/grype/db/v4/pkg/resolver/java/resolver.go index b9741f57150..c5933d78ef6 100644 --- a/grype/db/v4/pkg/resolver/java/resolver.go +++ b/grype/db/v4/pkg/resolver/java/resolver.go @@ -5,8 +5,8 @@ import ( "strings" grypePkg "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/packageurl-go" ) @@ -18,7 +18,7 @@ func (r *Resolver) Normalize(name string) string { } func (r *Resolver) Resolve(p grypePkg.Package) []string { - names := internal.NewStringSet() + 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/store/diff.go b/grype/db/v4/store/diff.go index 4cff4b9327a..88ed92f5b5c 100644 --- a/grype/db/v4/store/diff.go +++ b/grype/db/v4/store/diff.go @@ -5,8 +5,8 @@ import ( "github.com/wagoodman/go-progress" v4 "github.com/anchore/grype/grype/db/v4" - diffEvents "github.com/anchore/grype/grype/differ/events" "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/internal/bus" ) @@ -32,18 +32,21 @@ type storeMetadata struct { } // create manual progress bars for tracking the database diff's progress -func trackDiff() (*progress.Manual, *progress.Manual) { - rowsProcessed := progress.Manual{} - differencesDiscovered := progress.Manual{} +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: diffEvents.Monitor{ - RowsProcessed: progress.Monitorable(&rowsProcessed), - DifferencesDiscovered: progress.Monitorable(&differencesDiscovered), + Value: monitor.DBDiff{ + Stager: stager, + StageProgress: progress.Progressable(stageProgress), + DifferencesDiscovered: progress.Monitorable(differencesDiscovered), }, }) - return &rowsProcessed, &differencesDiscovered + return stageProgress, differencesDiscovered, stager } // creates a map from an unpackaged key to a list of all packages associated with it diff --git a/grype/db/v4/store/store.go b/grype/db/v4/store/store.go index 018a19b7909..e51281a4db7 100644 --- a/grype/db/v4/store/store.go +++ b/grype/db/v4/store/store.go @@ -4,14 +4,14 @@ import ( "fmt" "sort" + _ "github.com/glebarez/sqlite" // provide the sqlite dialect to gorm via import "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" - _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import + "github.com/anchore/grype/internal/stringutil" ) // store holds an instance of the database connection @@ -189,7 +189,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v4.VulnerabilityMetadata) e existing.Cvss = append(existing.Cvss, incomingCvss) } - links := internal.NewStringSetFromSlice(existing.URLs) + links := stringutil.NewStringSetFromSlice(existing.URLs) for _, l := range m.URLs { links.Add(l) } @@ -307,37 +307,45 @@ func (s *store) GetAllVulnerabilityMetadata() (*[]v4.VulnerabilityMetadata, erro // DiffStore creates a diff between the current sql database and the given store func (s *store) DiffStore(targetStore v4.StoreReader) (*[]v4.Diff, error) { - rowsProgress, diffItems := trackDiff() + // 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 diff --git a/grype/db/v5/namespace/index_test.go b/grype/db/v5/namespace/index_test.go index 64ce00c4a79..cac7b68aa87 100644 --- a/grype/db/v5/namespace/index_test.go +++ b/grype/db/v5/namespace/index_test.go @@ -30,6 +30,8 @@ func TestFromStringSlice(t *testing.T) { "nvd:cpe", "github:language:ruby", "abc.xyz:language:ruby", + "github:language:rust", + "something:language:rust", "1234.4567:language:unknown", "---:cpe", "another-provider:distro:alpine:3.15", @@ -44,6 +46,10 @@ func TestFromStringSlice(t *testing.T) { language.NewNamespace("github", syftPkg.Ruby, ""), language.NewNamespace("abc.xyz", syftPkg.Ruby, ""), }, + syftPkg.Rust: { + language.NewNamespace("github", syftPkg.Rust, ""), + language.NewNamespace("something", syftPkg.Rust, ""), + }, syftPkg.Language("unknown"): { language.NewNamespace("1234.4567", syftPkg.Language("unknown"), ""), }, 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 b1c7362bd37..a06e76dc64f 100644 --- a/grype/db/v5/pkg/qualifier/from_json.go +++ b/grype/db/v5/pkg/qualifier/from_json.go @@ -43,7 +43,7 @@ func FromJSON(data []byte) ([]Qualifier, error) { } qualifiers = append(qualifiers, q) default: - log.Warn("Skipping unsupported package qualifier: %s", k) + log.Debug("Skipping unsupported package qualifier: %s", k) continue } } diff --git a/grype/db/v5/pkg/resolver/java/resolver.go b/grype/db/v5/pkg/resolver/java/resolver.go index b9741f57150..c5933d78ef6 100644 --- a/grype/db/v5/pkg/resolver/java/resolver.go +++ b/grype/db/v5/pkg/resolver/java/resolver.go @@ -5,8 +5,8 @@ import ( "strings" grypePkg "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/packageurl-go" ) @@ -18,7 +18,7 @@ func (r *Resolver) Normalize(name string) string { } func (r *Resolver) Resolve(p grypePkg.Package) []string { - names := internal.NewStringSet() + 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/v5/store/diff.go b/grype/db/v5/store/diff.go index d1a2780027b..3aaddd9ada2 100644 --- a/grype/db/v5/store/diff.go +++ b/grype/db/v5/store/diff.go @@ -5,8 +5,8 @@ import ( "github.com/wagoodman/go-progress" v5 "github.com/anchore/grype/grype/db/v5" - diffEvents "github.com/anchore/grype/grype/differ/events" "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/internal/bus" ) @@ -32,18 +32,21 @@ type storeMetadata struct { } // create manual progress bars for tracking the database diff's progress -func trackDiff() (*progress.Manual, *progress.Manual) { - rowsProcessed := progress.Manual{} - differencesDiscovered := progress.Manual{} +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: diffEvents.Monitor{ - RowsProcessed: progress.Monitorable(&rowsProcessed), - DifferencesDiscovered: progress.Monitorable(&differencesDiscovered), + Value: monitor.DBDiff{ + Stager: stager, + StageProgress: progress.Progressable(stageProgress), + DifferencesDiscovered: progress.Monitorable(differencesDiscovered), }, }) - return &rowsProcessed, &differencesDiscovered + return stageProgress, differencesDiscovered, stager } // creates a map from an unpackaged key to a list of all packages associated with it diff --git a/grype/db/v5/store/store.go b/grype/db/v5/store/store.go index 725fa54ba26..748a229fd77 100644 --- a/grype/db/v5/store/store.go +++ b/grype/db/v5/store/store.go @@ -4,14 +4,14 @@ import ( "fmt" "sort" + _ "github.com/glebarez/sqlite" // provide the sqlite dialect to gorm via import "github.com/go-test/deep" "gorm.io/gorm" "github.com/anchore/grype/grype/db/internal/gormadapter" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/store/model" - "github.com/anchore/grype/internal" - _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import + "github.com/anchore/grype/internal/stringutil" ) // store holds an instance of the database connection @@ -207,7 +207,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v5.VulnerabilityMetadata) e existing.Cvss = append(existing.Cvss, incomingCvss) } - links := internal.NewStringSetFromSlice(existing.URLs) + links := stringutil.NewStringSetFromSlice(existing.URLs) for _, l := range m.URLs { links.Add(l) } @@ -325,37 +325,45 @@ func (s *store) GetAllVulnerabilityMetadata() (*[]v5.VulnerabilityMetadata, erro // DiffStore creates a diff between the current sql database and the given store func (s *store) DiffStore(targetStore v5.StoreReader) (*[]v5.Diff, error) { - rowsProgress, diffItems := trackDiff() + // 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 diff --git a/grype/db/v5/vulnerability_metadata.go b/grype/db/v5/vulnerability_metadata.go index 0176f6e9282..e37395cfd97 100644 --- a/grype/db/v5/vulnerability_metadata.go +++ b/grype/db/v5/vulnerability_metadata.go @@ -23,6 +23,8 @@ type Cvss struct { 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 + Source string `json:"source"` // Identifies the organization that provided the score + Type string `json:"type"` // Whether the source is a `primary` or `secondary` source } // CvssMetrics are the quantitative values that make up a CVSS score. diff --git a/grype/deprecated.go b/grype/deprecated.go index f67e1206355..5407d968665 100644 --- a/grype/deprecated.go +++ b/grype/deprecated.go @@ -5,13 +5,14 @@ import ( "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/store" + "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/source" ) -// TODO: deprecated, remove in v1.0.0 +// TODO: deprecated, will remove before v1.0.0 func FindVulnerabilities(store store.Store, userImageStr string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) (match.Matches, pkg.Context, []pkg.Package, error) { providerConfig := pkg.ProviderConfig{ SyftProviderConfig: pkg.SyftProviderConfig{ @@ -31,7 +32,20 @@ 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 +// TODO: deprecated, will remove before 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) + runner := VulnerabilityMatcher{ + Store: store, + 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/differ/differ.go b/grype/differ/differ.go index a74585307a2..af232875881 100644 --- a/grype/differ/differ.go +++ b/grype/differ/differ.go @@ -100,9 +100,7 @@ func (d *Differ) setOrDownload(curator *db.Curator, filenameOrURL string) error func download(curator *db.Curator, listing *db.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) diff --git a/grype/differ/events/events.go b/grype/differ/events/events.go deleted file mode 100644 index f24d6455dba..00000000000 --- a/grype/differ/events/events.go +++ /dev/null @@ -1,8 +0,0 @@ -package events - -import "github.com/wagoodman/go-progress" - -type Monitor struct { - RowsProcessed progress.Monitorable - DifferencesDiscovered progress.Monitorable -} diff --git a/grype/distro/distro_test.go b/grype/distro/distro_test.go index 8b6cecdfc28..c757119c53f 100644 --- a/grype/distro/distro_test.go +++ b/grype/distro/distro_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" ) @@ -214,8 +214,8 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) { }, } - observedDistros := internal.NewStringSet() - definedDistros := internal.NewStringSet() + observedDistros := stringutil.NewStringSet() + definedDistros := stringutil.NewStringSet() for _, distroType := range All { definedDistros.Add(string(distroType)) @@ -227,7 +227,7 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) { for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { - s, err := source.NewFromDirectory(test.fixture) + s, err := source.NewFromDirectory(source.DirectoryConfig{Path: test.fixture}) require.NoError(t, err) resolver, err := s.FileResolver(source.SquashedScope) diff --git a/grype/event/event.go b/grype/event/event.go index 91cfcde1a01..6f0e36f8861 100644 --- a/grype/event/event.go +++ b/grype/event/event.go @@ -1,12 +1,27 @@ package event -import "github.com/wagoodman/go-partybus" +import ( + "github.com/wagoodman/go-partybus" +) const ( - AppUpdateAvailable partybus.EventType = "grype-app-update-available" - UpdateVulnerabilityDatabase partybus.EventType = "grype-update-vulnerability-database" - VulnerabilityScanningStarted partybus.EventType = "grype-vulnerability-scanning-started" - VulnerabilityScanningFinished partybus.EventType = "grype-vulnerability-scanning-finished" - NonRootCommandFinished partybus.EventType = "grype-non-root-command-finished" - DatabaseDiffingStarted partybus.EventType = "grype-database-diffing-started" + typePrefix = "grype" + cliTypePrefix = typePrefix + "-cli" + + // Events from the grype library + + UpdateVulnerabilityDatabase partybus.EventType = typePrefix + "-update-vulnerability-database" + VulnerabilityScanningStarted partybus.EventType = typePrefix + "-vulnerability-scanning-started" + DatabaseDiffingStarted partybus.EventType = typePrefix + "-database-diffing-started" + + // Events exclusively for the CLI + + // CLIAppUpdateAvailable is a partybus event that occurs when an application update is available + CLIAppUpdateAvailable partybus.EventType = cliTypePrefix + "-app-update-available" + + // CLIReport is a partybus event that occurs when an analysis result is ready for final presentation to stdout + CLIReport partybus.EventType = cliTypePrefix + "-report" + + // CLINotification is a partybus event that occurs when auxiliary information is ready for presentation to stderr + CLINotification partybus.EventType = cliTypePrefix + "-notification" ) diff --git a/grype/event/monitor/db_diff.go b/grype/event/monitor/db_diff.go new file mode 100644 index 00000000000..3f09bc2d52e --- /dev/null +++ b/grype/event/monitor/db_diff.go @@ -0,0 +1,9 @@ +package monitor + +import "github.com/wagoodman/go-progress" + +type DBDiff struct { + Stager progress.Stager + StageProgress progress.Progressable + DifferencesDiscovered progress.Monitorable +} diff --git a/grype/event/monitor/matching.go b/grype/event/monitor/matching.go new file mode 100644 index 00000000000..f8280b09e36 --- /dev/null +++ b/grype/event/monitor/matching.go @@ -0,0 +1,16 @@ +package monitor + +import ( + "github.com/wagoodman/go-progress" + + "github.com/anchore/grype/grype/vulnerability" +) + +type Matching struct { + 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 9b1a3c14155..9606ef1a95f 100644 --- a/grype/event/parsers/parsers.go +++ b/grype/event/parsers/parsers.go @@ -6,10 +6,8 @@ import ( "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" - diffEvents "github.com/anchore/grype/grype/differ/events" "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/grype/matcher" - "github.com/anchore/grype/grype/presenter" + "github.com/anchore/grype/grype/event/monitor" ) type ErrBadPayload struct { @@ -37,19 +35,6 @@ func checkEventType(actual, expected partybus.EventType) error { return nil } -func ParseAppUpdateAvailable(e partybus.Event) (string, error) { - if err := checkEventType(e.Type, event.AppUpdateAvailable); err != nil { - return "", err - } - - newVersion, ok := e.Value.(string) - if !ok { - return "", newPayloadErr(e.Type, "Value", e.Value) - } - - return newVersion, nil -} - func ParseUpdateVulnerabilityDatabase(e partybus.Event) (progress.StagedProgressable, error) { if err := checkEventType(e.Type, event.UpdateVulnerabilityDatabase); err != nil { return nil, err @@ -63,54 +48,84 @@ func ParseUpdateVulnerabilityDatabase(e partybus.Event) (progress.StagedProgress return prog, nil } -func ParseVulnerabilityScanningStarted(e partybus.Event) (*matcher.Monitor, error) { +func ParseVulnerabilityScanningStarted(e partybus.Event) (*monitor.Matching, error) { if err := checkEventType(e.Type, event.VulnerabilityScanningStarted); err != nil { return nil, err } - monitor, ok := e.Value.(matcher.Monitor) + mon, ok := e.Value.(monitor.Matching) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } - return &monitor, nil + return &mon, nil } -func ParseVulnerabilityScanningFinished(e partybus.Event) (presenter.Presenter, error) { - if err := checkEventType(e.Type, event.VulnerabilityScanningFinished); err != nil { +func ParseDatabaseDiffingStarted(e partybus.Event) (*monitor.DBDiff, error) { + if err := checkEventType(e.Type, event.DatabaseDiffingStarted); err != nil { return nil, err } - pres, ok := e.Value.(presenter.Presenter) + mon, ok := e.Value.(monitor.DBDiff) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } - return pres, nil + return &mon, nil +} + +type UpdateCheck struct { + New string + Current string } -func ParseNonRootCommandFinished(e partybus.Event) (*string, error) { - if err := checkEventType(e.Type, event.NonRootCommandFinished); err != nil { +func ParseCLIAppUpdateAvailable(e partybus.Event) (*UpdateCheck, error) { + if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil { return nil, err } - result, ok := e.Value.(string) + updateCheck, ok := e.Value.(UpdateCheck) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } - return &result, nil + return &updateCheck, nil } -func ParseDatabaseDiffingStarted(e partybus.Event) (*diffEvents.Monitor, error) { - if err := checkEventType(e.Type, event.DatabaseDiffingStarted); err != nil { - return nil, err +func ParseCLIReport(e partybus.Event) (string, string, error) { + if err := checkEventType(e.Type, event.CLIReport); err != nil { + return "", "", err } - monitor, ok := e.Value.(diffEvents.Monitor) + context, ok := e.Source.(string) if !ok { - return nil, newPayloadErr(e.Type, "Value", e.Value) + // this is optional + context = "" + } + + report, ok := e.Value.(string) + if !ok { + return "", "", newPayloadErr(e.Type, "Value", e.Value) + } + + return context, report, nil +} + +func ParseCLINotification(e partybus.Event) (string, string, error) { + if err := checkEventType(e.Type, event.CLINotification); err != nil { + return "", "", err + } + + context, ok := e.Source.(string) + if !ok { + // this is optional + context = "" + } + + notification, ok := e.Value.(string) + if !ok { + return "", "", newPayloadErr(e.Type, "Value", e.Value) } - return &monitor, nil + return context, notification, nil } diff --git a/grype/lib.go b/grype/lib.go index 25cddd9c5ad..53a499a2515 100644 --- a/grype/lib.go +++ b/grype/lib.go @@ -8,10 +8,10 @@ import ( "github.com/anchore/grype/internal/log" ) -func SetLogger(logger logger.Logger) { - log.Log = logger +func SetLogger(l logger.Logger) { + log.Set(l) } func SetBus(b *partybus.Bus) { - bus.SetPublisher(b) + bus.Set(b) } diff --git a/grype/match/explicit_ignores.go b/grype/match/explicit_ignores.go index 2c29a3f840c..0ec272b1a90 100644 --- a/grype/match/explicit_ignores.go +++ b/grype/match/explicit_ignores.go @@ -18,7 +18,7 @@ func init() { // https://github.com/mergebase/log4j-samples collection, we want to filter these explicitly: { typ: "java-archive", - vulnerabilities: []string{"CVE-2021-44228", "CVE-2021-45046", "GHSA-jfh8-c2jp-5v3q", "GHSA-7rjr-3q55-vv33"}, + vulnerabilities: []string{"CVE-2021-44228", "CVE-2021-45046", "GHSA-jfh8-c2jp-5v3q", "GHSA-7rjr-3q55-vv33", "CVE-2020-9493", "CVE-2022-23307", "CVE-2023-26464"}, packages: []string{"log4j-api", "log4j-slf4j-impl", "log4j-to-slf4j", "log4j-1.2-api", "log4j-detector", "log4j-over-slf4j", "slf4j-log4j12"}, }, // Based on https://github.com/anchore/grype/issues/558: @@ -69,7 +69,7 @@ func init() { } // ApplyExplicitIgnoreRules Filters out matches meeting the criteria defined above and those within the grype database -func ApplyExplicitIgnoreRules(provider ExclusionProvider, matches Matches) Matches { +func ApplyExplicitIgnoreRules(provider ExclusionProvider, matches Matches) (Matches, []IgnoredMatch) { var ignoreRules []IgnoreRule ignoreRules = append(ignoreRules, explicitIgnoreRules...) @@ -84,18 +84,5 @@ func ApplyExplicitIgnoreRules(provider ExclusionProvider, matches Matches) Match ignoreRules = append(ignoreRules, r...) } - matches, ignored := ApplyIgnoreRules(matches, ignoreRules) - - 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) - } - } - - return matches + return ApplyIgnoreRules(matches, ignoreRules) } diff --git a/grype/match/explicit_ignores_test.go b/grype/match/explicit_ignores_test.go index 79af5e7256c..c95930de7c4 100644 --- a/grype/match/explicit_ignores_test.go +++ b/grype/match/explicit_ignores_test.go @@ -39,6 +39,7 @@ func Test_ApplyExplicitIgnoreRules(t *testing.T) { typ syftPkg.Type matches []cvePkg expected []string + ignored []string }{ // some explicit log4j-related data: // "CVE-2021-44228", "CVE-2021-45046", "GHSA-jfh8-c2jp-5v3q", "GHSA-7rjr-3q55-vv33", @@ -69,6 +70,7 @@ func Test_ApplyExplicitIgnoreRules(t *testing.T) { {"CVE-2021-44228", "log4j-core"}, }, expected: []string{"log4j-core"}, + ignored: []string{"log4j-api"}, }, { name: "filters all matching CVEs and packages", @@ -78,6 +80,7 @@ func Test_ApplyExplicitIgnoreRules(t *testing.T) { {"GHSA-jfh8-c2jp-5v3q", "log4j-slf4j-impl"}, }, expected: []string{}, + ignored: []string{"log4j-api", "log4j-slf4j-impl"}, }, { name: "filters invalid CVEs for protobuf Go module", @@ -87,6 +90,7 @@ func Test_ApplyExplicitIgnoreRules(t *testing.T) { {"CVE-2021-22570", "google.golang.org/protobuf"}, }, expected: []string{}, + ignored: []string{"google.golang.org/protobuf", "google.golang.org/protobuf"}, }, { name: "keeps valid CVEs for protobuf Go module", @@ -118,7 +122,7 @@ func Test_ApplyExplicitIgnoreRules(t *testing.T) { }) } - filtered := ApplyExplicitIgnoreRules(p, matches) + filtered, ignores := ApplyExplicitIgnoreRules(p, matches) var found []string for match := range filtered.Enumerate() { @@ -126,6 +130,16 @@ func Test_ApplyExplicitIgnoreRules(t *testing.T) { } assert.ElementsMatch(t, test.expected, found) + + if len(test.ignored) > 0 { + var ignored []string + for _, i := range ignores { + ignored = append(ignored, i.Package.Name) + } + assert.ElementsMatch(t, test.ignored, ignored) + } else { + assert.Empty(t, ignores) + } }) } } diff --git a/grype/match/ignore.go b/grype/match/ignore.go index cd719a70392..5984938d83d 100644 --- a/grype/match/ignore.go +++ b/grype/match/ignore.go @@ -17,10 +17,12 @@ 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"` + 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"` } // IgnoreRulePackage describes the Package-specific fields that comprise the IgnoreRule. @@ -67,6 +69,11 @@ func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMa } func shouldIgnore(match Match, rule IgnoreRule) bool { + // VEX rules are handled by the vex processor + if rule.VexStatus != "" { + return false + } + ignoreConditions := getIgnoreConditionsForRule(rule) if len(ignoreConditions) == 0 { // this rule specifies no criteria, so it doesn't apply to the Match @@ -84,6 +91,12 @@ func shouldIgnore(match Match, rule IgnoreRule) bool { return true } +// HasConditions returns true if the ignore rule has conditions +// that can cause a match to be ignored +func (ir IgnoreRule) HasConditions() bool { + return len(getIgnoreConditionsForRule(ir)) == 0 +} + // An ignoreCondition is a function that returns a boolean indicating whether // the given Match should be ignored. type ignoreCondition func(match Match) bool diff --git a/grype/match/ignore_test.go b/grype/match/ignore_test.go index 4490bf8a736..57af7c53d20 100644 --- a/grype/match/ignore_test.go +++ b/grype/match/ignore_test.go @@ -9,8 +9,8 @@ import ( 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" syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" ) var ( @@ -28,7 +28,7 @@ var ( Name: "dive", Version: "0.5.2", Type: "deb", - Locations: source.NewLocationSet(source.NewLocation("/path/that/has/dive")), + Locations: file.NewLocationSet(file.NewLocation("/path/that/has/dive")), }, }, { @@ -45,7 +45,7 @@ var ( Version: "100.0.50", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, - Locations: source.NewLocationSet(source.NewVirtualLocation("/real/path/with/reach", + Locations: file.NewLocationSet(file.NewVirtualLocation("/real/path/with/reach", "/virtual/path/that/has/reach")), }, }, @@ -63,7 +63,7 @@ var ( Version: "100.0.51", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, - Locations: source.NewLocationSet(source.NewVirtualLocation("/real/path/with/beach", + Locations: file.NewLocationSet(file.NewVirtualLocation("/real/path/with/beach", "/virtual/path/that/has/beach")), }, }, @@ -81,7 +81,7 @@ var ( Version: "100.0.52", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, - Locations: source.NewLocationSet(source.NewVirtualLocation("/real/path/with/speach", + Locations: file.NewLocationSet(file.NewVirtualLocation("/real/path/with/speach", "/virtual/path/that/has/speach")), }, }, @@ -337,9 +337,9 @@ var ( ID: pkg.ID(uuid.NewString()), Name: "a-pkg", Version: "1.0", - Locations: source.NewLocationSet( - source.NewLocation("/some/path"), - source.NewVirtualLocation("/some/path", "/some/virtual/path"), + Locations: file.NewLocationSet( + file.NewLocation("/some/path"), + file.NewVirtualLocation("/some/path", "/some/virtual/path"), ), Type: "rpm", }, diff --git a/grype/match/match.go b/grype/match/match.go index 128ceae169f..d7982335ef6 100644 --- a/grype/match/match.go +++ b/grype/match/match.go @@ -26,10 +26,6 @@ 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, diff --git a/grype/match/matcher_type.go b/grype/match/matcher_type.go index 6b596a88521..ad547c6d94c 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,8 @@ var AllMatcherTypes = []MatcherType{ MsrcMatcher, PortageMatcher, GoModuleMatcher, + OpenVexMatcher, + RustMatcher, } type MatcherType string diff --git a/grype/match/matches.go b/grype/match/matches.go index 0df9a977884..e469c6dd3a1 100644 --- a/grype/match/matches.go +++ b/grype/match/matches.go @@ -52,6 +52,16 @@ func (r *Matches) Merge(other Matches) { } } +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) { if len(matches) == 0 { return diff --git a/grype/match/matches_test.go b/grype/match/matches_test.go index 59d829335c4..b26e21c3c14 100644 --- a/grype/match/matches_test.go +++ b/grype/match/matches_test.go @@ -290,3 +290,67 @@ 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{ + ID: "vuln-a", + Namespace: "name-a", + }, + Package: pkg.Package{ + ID: "package-a", + }, + } + + b := Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "vuln-b", + Namespace: "name-b", + }, + Package: pkg.Package{ + ID: "package-b", + }, + } + + c := Match{ + Vulnerability: vulnerability.Vulnerability{ + 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) + }) + } +} diff --git a/grype/matcher/apk/matcher_test.go b/grype/matcher/apk/matcher_test.go index a9b303aa793..569b94a07fb 100644 --- a/grype/matcher/apk/matcher_test.go +++ b/grype/matcher/apk/matcher_test.go @@ -350,6 +350,10 @@ func TestNvdOnlyMatches(t *testing.T) { SearchedBy: search.CPEParameters{ CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", + Package: search.CPEPackageParameter{ + Name: "libvncserver", + Version: "0.9.9", + }, }, Found: search.CPEResult{ CPEs: []string{vulnFound.CPEs[0].BindToFmtString()}, @@ -425,6 +429,10 @@ func TestNvdMatchesProperVersionFiltering(t *testing.T) { SearchedBy: search.CPEParameters{ CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", + Package: search.CPEPackageParameter{ + Name: "libvncserver", + Version: "0.9.11-r10", + }, }, Found: search.CPEResult{ CPEs: []string{vulnFound.CPEs[0].BindToFmtString()}, @@ -679,6 +687,10 @@ func TestNVDMatchBySourceIndirection(t *testing.T) { SearchedBy: search.CPEParameters{ CPEs: []string{"cpe:2.3:a:musl:musl:*:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", + Package: search.CPEPackageParameter{ + Name: "musl", + Version: "1.3.2-r0", + }, }, Found: search.CPEResult{ CPEs: []string{vulnFound.CPEs[0].BindToFmtString()}, diff --git a/grype/matcher/dpkg/matcher_test.go b/grype/matcher/dpkg/matcher_test.go index 054855641fe..b04a5c477de 100644 --- a/grype/matcher/dpkg/matcher_test.go +++ b/grype/matcher/dpkg/matcher_test.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -39,7 +39,7 @@ func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) { assert.Len(t, actual, 2, "unexpected indirect matches count") - foundCVEs := internal.NewStringSet() + foundCVEs := stringutil.NewStringSet() for _, a := range actual { foundCVEs.Add(a.Vulnerability.ID) diff --git a/grype/matcher/java/matcher_test.go b/grype/matcher/java/matcher_test.go index d80a8c0073c..b3dcdf64371 100644 --- a/grype/matcher/java/matcher_test.go +++ b/grype/matcher/java/matcher_test.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -44,7 +44,7 @@ func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) { assert.Len(t, actual, 2, "unexpected matches count") - foundCVEs := internal.NewStringSet() + foundCVEs := stringutil.NewStringSet() for _, v := range actual { foundCVEs.Add(v.Vulnerability.ID) diff --git a/grype/matcher/matchers.go b/grype/matcher/matchers.go index a7e060fd875..72778eb6292 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/match" "github.com/anchore/grype/grype/matcher/apk" "github.com/anchore/grype/grype/matcher/dotnet" "github.com/anchore/grype/grype/matcher/dpkg" @@ -19,65 +12,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 Monitor struct { - PackagesProcessed progress.Monitorable - VulnerabilitiesDiscovered progress.Monitorable - Fixed progress.Monitorable - BySeverity map[vulnerability.Severity]progress.Monitorable -} - -type monitor struct { - PackagesProcessed *progress.Manual - VulnerabilitiesDiscovered *progress.Manual - Fixed *progress.Manual - BySeverity map[vulnerability.Severity]*progress.Manual -} - -func newMonitor() (monitor, Monitor) { - manualBySev := make(map[vulnerability.Severity]*progress.Manual) - for _, severity := range vulnerability.AllSeverities() { - manualBySev[severity] = progress.NewManual(-1) - } - manualBySev[vulnerability.UnknownSeverity] = progress.NewManual(-1) - - m := monitor{ - 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{ - PackagesProcessed: m.PackagesProcessed, - VulnerabilitiesDiscovered: m.VulnerabilitiesDiscovered, - Fixed: m.Fixed, - BySeverity: monitorableBySev, - } -} - -func (m *monitor) 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 @@ -86,6 +24,7 @@ type Config struct { Dotnet dotnet.MatcherConfig Javascript javascript.MatcherConfig Golang golang.MatcherConfig + Rust rust.MatcherConfig Stock stock.MatcherConfig } @@ -102,150 +41,7 @@ func NewDefaultMatchers(mc Config) []Matcher { golang.NewGolangMatcher(mc.Golang), &msrc.Matcher{}, &portage.Matcher{}, + rust.NewRustMatcher(mc.Rust), stock.NewStockMatcher(mc.Stock), } } - -func trackMatcher() *monitor { - 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 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 { - logMatches(p, matches) - res.Add(matches...) - progressMonitor.VulnerabilitiesDiscovered.Add(int64(len(matches))) - updateVulnerabilityList(progressMonitor, matches, store) - } - } - } - - progressMonitor.SetCompleted() - - logListSummary(progressMonitor) - - // Filter out matches based off of the records in the exclusion table in the database or from the old hard-coded rules - res = match.ApplyExplicitIgnoreRules(store, res) - - return res -} - -func logListSummary(vl *monitor) { - 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 updateVulnerabilityList(list *monitor, 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/portage/matcher_test.go b/grype/matcher/portage/matcher_test.go index f3f691a0cc2..2c3c769a59f 100644 --- a/grype/matcher/portage/matcher_test.go +++ b/grype/matcher/portage/matcher_test.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -33,7 +33,7 @@ func TestMatcherPortage_Match(t *testing.T) { assert.Len(t, actual, 1, "unexpected indirect matches count") - foundCVEs := internal.NewStringSet() + foundCVEs := stringutil.NewStringSet() for _, a := range actual { foundCVEs.Add(a.Vulnerability.ID) diff --git a/grype/matcher/rust/matcher.go b/grype/matcher/rust/matcher.go new file mode 100644 index 00000000000..6ce17449302 --- /dev/null +++ b/grype/matcher/rust/matcher.go @@ -0,0 +1,40 @@ +package rust + +import ( + "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/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, 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...) +} diff --git a/grype/pkg/context.go b/grype/pkg/context.go index 4a2e65b56c9..5f46a6f9f9c 100644 --- a/grype/pkg/context.go +++ b/grype/pkg/context.go @@ -6,6 +6,6 @@ import ( ) type Context struct { - Source *source.Metadata + Source *source.Description Distro *linux.Release } diff --git a/grype/pkg/package.go b/grype/pkg/package.go index 3dcd423b945..5b65015a86d 100644 --- a/grype/pkg/package.go +++ b/grype/pkg/package.go @@ -5,13 +5,14 @@ import ( "regexp" "strings" - "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" cpes "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" - "github.com/anchore/syft/syft/source" ) // the source-rpm field has something akin to "util-linux-ng-2.17.2-12.28.el6_9.2.src.rpm" @@ -29,10 +30,10 @@ 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 source.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) + 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 @@ -101,7 +102,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 *pkg.Collection, relationships []artifact.Relationship, distro *linux.Release) *pkg.Collection { byOverlap := map[artifact.ID]artifact.Relationship{} for _, r := range relationships { if r.Type == artifact.OwnershipByFileOverlapRelationship { @@ -110,12 +111,12 @@ func removePackagesByOverlap(catalog *pkg.Collection, relationships []artifact.R } out := pkg.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) { + if ok && excludePackage(comprehensiveDistroFeed, p, from) { continue } } @@ -125,7 +126,7 @@ func removePackagesByOverlap(catalog *pkg.Collection, relationships []artifact.R return out } -func excludePackage(p pkg.Package, parent pkg.Package) bool { +func excludePackage(comprehensiveDistroFeed bool, p pkg.Package, parent pkg.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 @@ -135,14 +136,68 @@ func excludePackage(p pkg.Package, parent pkg.Package) bool { 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 != pkg.BinaryPkg { return false } return true } +// distroFeedIsComprehensive returns true if the distro feed +// is comprehensive enough that we can drop packages owned by distro packages +// before matching. +func distroFeedIsComprehensive(distro *linux.Release) bool { + // TODO: this mechanism should be re-examined once https://github.com/anchore/grype/issues/1426 + // is addressed + if distro == nil { + return false + } + if distro.ID == "amzn" { + // AmazonLinux shows "like rhel" but is not an rhel clone + // and does not have an exhaustive vulnerability feed. + return false + } + for _, d := range comprehensiveDistros { + if strings.EqualFold(d, distro.ID) { + return true + } + for _, n := range distro.IDLike { + if strings.EqualFold(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' and replacing 'redhat' with 'rhel' +var comprehensiveDistros = []string{ + "debian", + "mariner", + "rhel", + "ubuntu", +} + +func isOSPackage(p pkg.Package) bool { + switch p.Type { + case pkg.DebPkg, pkg.RpmPkg, pkg.PortagePkg, pkg.AlpmPkg, pkg.ApkPkg: + return true + default: + return false + } +} + func dataFromPkg(p pkg.Package) (MetadataType, interface{}, []UpstreamPackage) { var metadata interface{} var upstreams []UpstreamPackage @@ -231,7 +286,7 @@ func rpmDataFromPkg(p pkg.Package) (metadata *RpmMetadata, upstreams []UpstreamP } func getNameAndELVersion(sourceRpm string) (string, string) { - groupMatches := internal.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm) + groupMatches := stringutil.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm) version := groupMatches["version"] + "-" + groupMatches["release"] return groupMatches["name"], version } diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go index 2e4a5703e08..d294ce7b385 100644 --- a/grype/pkg/package_test.go +++ b/grype/pkg/package_test.go @@ -13,8 +13,9 @@ import ( "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/source" + "github.com/anchore/syft/syft/sbom" ) func TestNew(t *testing.T) { @@ -424,7 +425,7 @@ func TestNew(t *testing.T) { Extras: []string{"a"}, VersionConstraint: "a", URL: "a", - Markers: map[string]string{"a": "a"}, + Markers: "a", }, }, }, @@ -515,6 +516,30 @@ func TestNew(t *testing.T) { }, }, }, + { + name: "dotnet-portable-executable-metadata", + syftPkg: syftPkg.Package{ + MetadataType: syftPkg.DotnetPortableExecutableMetadataType, + Metadata: syftPkg.DotnetPortableExecutableMetadata{ + AssemblyVersion: "a", + LegalCopyright: "a", + Comments: "a", + InternalName: "a", + CompanyName: "a", + ProductName: "a", + ProductVersion: "a", + }, + }, + }, + { + name: "dotnet-portable-executable-metadata", + syftPkg: syftPkg.Package{ + MetadataType: syftPkg.SwiftPackageManagerMetadataType, + Metadata: syftPkg.SwiftPackageManagerMetadata{ + Revision: "a", + }, + }, + }, } // capture each observed metadata type, we should see all of them relate to what syft provides by the end of testing @@ -550,8 +575,8 @@ func TestFromCollection_DoesNotPanic(t *testing.T) { examplePackage := syftPkg.Package{ Name: "test", Version: "1.2.3", - Locations: source.NewLocationSet( - source.NewLocation("/test-path"), + Locations: file.NewLocationSet( + file.NewLocation("/test-path"), ), Type: syftPkg.NpmPkg, } @@ -633,10 +658,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 }{ { @@ -653,11 +678,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"}, }, { @@ -667,10 +706,38 @@ 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: withDistro(catalogWithOverlaps( + []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, + []string{"rpm:python3-rpm@4.14.3-26.el8 -> python:rpm@4.14.3"}), "rhel"), + expectedPackages: []string{"rpm:python3-rpm@4.14.3-26.el8"}, + }, + { + name: "amzn linux doesn't remove packages in this way", + sbom: withDistro(catalogWithOverlaps( + []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"}, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - catalog := removePackagesByOverlap(test.sbom.collection, test.sbom.relationships) + catalog := removePackagesByOverlap(test.sbom.Artifacts.Packages, test.sbom.Relationships, test.sbom.Artifacts.LinuxDistribution) pkgs := FromCollection(catalog, SynthesisConfig{}) var pkgNames []string for _, p := range pkgs { @@ -681,12 +748,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 @@ -735,8 +797,17 @@ func catalogWithOverlaps(packages []string, overlaps []string) catalogRelationsh catalog := syftPkg.NewCollection(pkgs...) - return catalogRelationships{ - collection: catalog, - relationships: relationships, + return &sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: catalog, + }, + Relationships: relationships, + } +} + +func withDistro(s *sbom.SBOM, id string) *sbom.SBOM { + s.Artifacts.LinuxDistribution = &linux.Release{ + ID: id, } + return s } diff --git a/grype/pkg/provider.go b/grype/pkg/provider.go index b7721acbc19..a3356b7ace1 100644 --- a/grype/pkg/provider.go +++ b/grype/pkg/provider.go @@ -6,8 +6,8 @@ import ( "github.com/bmatcuk/doublestar/v2" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/sbom" - "github.com/anchore/syft/syft/source" ) var errDoesNotProvide = fmt.Errorf("cannot provide packages from the given source") @@ -25,9 +25,9 @@ func Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom return packages, ctx, s, err } - packages, ctx, err = purlProvider(userInput) + packages, err = purlProvider(userInput) if !errors.Is(err, errDoesNotProvide) { - return packages, ctx, s, err + return packages, Context{}, s, err } return syftProvider(userInput, config) @@ -70,7 +70,7 @@ func filterPackageExclusions(packages []Package, exclusions []string) ([]Package // Test a location RealPath and VirtualPath for a match against the exclusion parameter. // The exclusion allows glob expressions such as `/usr/**` or `**/*.json`. If the exclusion // is an invalid pattern, an error is returned; otherwise, the resulting boolean indicates a match. -func locationMatches(location source.Location, exclusion string) (bool, error) { +func locationMatches(location file.Location, exclusion string) (bool, error) { matchesRealPath, err := doublestar.Match(exclusion, location.RealPath) if err != nil { return false, err diff --git a/grype/pkg/provider_test.go b/grype/pkg/provider_test.go index a9aaf1f1899..34dd94432c5 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/file" "github.com/anchore/syft/syft/pkg/cataloger" - "github.com/anchore/syft/syft/source" ) func TestProviderLocationExcludes(t *testing.T) { @@ -158,10 +158,10 @@ func Test_filterPackageExclusions(t *testing.T) { t.Run(test.name, func(t *testing.T) { var packages []Package for _, pkg := range test.locations { - locations := source.NewLocationSet() + locations := file.NewLocationSet() for _, l := range pkg { locations.Add( - source.NewVirtualLocation(l, l), + file.NewVirtualLocation(l, l), ) } packages = append(packages, Package{Locations: locations}) @@ -221,7 +221,7 @@ func Test_matchesLocation(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - matches, err := locationMatches(source.NewVirtualLocation(test.realPath, test.virtualPath), test.match) + matches, err := locationMatches(file.NewVirtualLocation(test.realPath, test.virtualPath), test.match) assert.NoError(t, err) assert.Equal(t, test.expected, matches) }) diff --git a/grype/pkg/purl_provider.go b/grype/pkg/purl_provider.go index 52606bad3e0..8032e545038 100644 --- a/grype/pkg/purl_provider.go +++ b/grype/pkg/purl_provider.go @@ -28,9 +28,9 @@ func (e errEmptyPurlFile) Error() string { return fmt.Sprintf("purl file is empty: %s", e.purlFilepath) } -func purlProvider(userInput string) ([]Package, Context, error) { +func purlProvider(userInput string) ([]Package, error) { p, err := getPurlPackages(userInput) - return p, Context{}, err + return p, err } func getPurlPackages(userInput string) ([]Package, error) { diff --git a/grype/pkg/purl_provider_test.go b/grype/pkg/purl_provider_test.go index 16cee07f705..c01ddfca3ad 100644 --- a/grype/pkg/purl_provider_test.go +++ b/grype/pkg/purl_provider_test.go @@ -23,7 +23,7 @@ func Test_PurlProvider_Fails(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { //WHEN - packages, _, err := purlProvider(tc.userInput) + packages, err := purlProvider(tc.userInput) //THEN assert.Nil(t, packages) @@ -38,7 +38,7 @@ func Test_CsvProvide(t *testing.T) { expected := []string{"curl", "ant", "log4j-core"} //WHEN - packages, _, err := purlProvider("purl:test-fixtures/valid-purl.txt") + packages, err := purlProvider("purl:test-fixtures/valid-purl.txt") //THEN packageNames := []string{} diff --git a/grype/pkg/syft_provider.go b/grype/pkg/syft_provider.go index 8b541ba2a60..0f1b725b0c6 100644 --- a/grype/pkg/syft_provider.go +++ b/grype/pkg/syft_provider.go @@ -1,42 +1,44 @@ package pkg import ( + "github.com/anchore/grype/internal/log" + "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { - if config.CatalogingOptions.Search.Scope == "" { - return nil, Context{}, nil, errDoesNotProvide - } - - sourceInput, err := source.ParseInputWithName(userInput, config.Platform, config.Name, config.DefaultImagePullSource) + src, err := getSource(userInput, config) if err != nil { return nil, Context{}, nil, err } - src, cleanup, err := source.New(*sourceInput, config.RegistryOptions, config.Exclusions) - if err != nil { - return nil, Context{}, nil, err - } - defer cleanup() + defer func() { + if src != nil { + if err := src.Close(); err != nil { + log.Tracef("unable to close source: %+v", err) + } + } + }() catalog, relationships, theDistro, err := syft.CatalogPackages(src, config.CatalogingOptions) if err != nil { return nil, Context{}, nil, err } - catalog = removePackagesByOverlap(catalog, relationships) + catalog = removePackagesByOverlap(catalog, relationships, theDistro) + + srcDescription := src.Describe() packages := FromCollection(catalog, config.SynthesisConfig) context := Context{ - Source: &src.Metadata, + Source: &srcDescription, Distro: theDistro, } sbom := &sbom.SBOM{ - Source: src.Metadata, + Source: srcDescription, Relationships: relationships, Artifacts: sbom.Artifacts{ Packages: catalog, @@ -45,3 +47,35 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, return packages, context, sbom, nil } + +func getSource(userInput string, config ProviderConfig) (source.Source, error) { + if config.CatalogingOptions.Search.Scope == "" { + return nil, errDoesNotProvide + } + + detection, err := source.Detect(userInput, source.DetectConfig{ + DefaultImageSource: config.DefaultImagePullSource, + }) + if err != nil { + return nil, err + } + + var platform *image.Platform + if config.Platform != "" { + platform, err = image.NewPlatform(config.Platform) + if err != nil { + return nil, err + } + } + + return detection.NewSource(source.DetectionSourceConfig{ + Alias: source.Alias{ + Name: config.Name, + }, + RegistryOptions: config.RegistryOptions, + Platform: platform, + Exclude: source.ExcludeConfig{ + Paths: config.Exclusions, + }, + }) +} diff --git a/grype/pkg/syft_sbom_provider.go b/grype/pkg/syft_sbom_provider.go index e316925c684..0157a4d708e 100644 --- a/grype/pkg/syft_sbom_provider.go +++ b/grype/pkg/syft_sbom_provider.go @@ -30,8 +30,7 @@ func syftSBOMProvider(userInput string, config ProviderConfig) ([]Package, Conte return nil, Context{}, nil, err } - catalog := s.Artifacts.Packages - catalog = removePackagesByOverlap(catalog, s.Relationships) + catalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, s.Artifacts.LinuxDistribution) return FromCollection(catalog, config.SynthesisConfig), Context{ Source: &s.Source, diff --git a/grype/pkg/syft_sbom_provider_test.go b/grype/pkg/syft_sbom_provider_test.go index 774dda2a874..4d1b0a12164 100644 --- a/grype/pkg/syft_sbom_provider_test.go +++ b/grype/pkg/syft_sbom_provider_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" ) @@ -26,8 +27,8 @@ func TestParseSyftJSON(t *testing.T) { { Name: "alpine-baselayout", Version: "3.2.0-r6", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759", }), @@ -50,8 +51,8 @@ func TestParseSyftJSON(t *testing.T) { { Name: "fake", Version: "1.2.0", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c", }), @@ -76,8 +77,8 @@ func TestParseSyftJSON(t *testing.T) { { Name: "gmp", Version: "6.2.0-r0", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c", }), @@ -101,11 +102,10 @@ func TestParseSyftJSON(t *testing.T) { }, }, Context: Context{ - Source: &source.Metadata{ - Scheme: source.ImageScheme, - ImageMetadata: source.ImageMetadata{ + Source: &source.Description{ + Metadata: source.StereoscopeImageSourceMetadata{ UserInput: "alpine:fake", - Layers: []source.LayerMetadata{ + Layers: []source.StereoscopeLayerMetadata{ { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Digest: "sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a", @@ -120,7 +120,6 @@ func TestParseSyftJSON(t *testing.T) { "alpine:fake", }, }, - Path: "", }, Distro: &linux.Release{ Name: "alpine", @@ -138,8 +137,12 @@ func TestParseSyftJSON(t *testing.T) { t.Fatalf("unable to parse: %+v", err) } - context.Source.ImageMetadata.RawConfig = nil - context.Source.ImageMetadata.RawManifest = nil + if m, ok := context.Source.Metadata.(source.StereoscopeImageSourceMetadata); ok { + m.RawConfig = nil + m.RawManifest = nil + + context.Source.Metadata = m + } for _, d := range deep.Equal(test.Packages, pkgs) { if strings.Contains(d, ".ID: ") { @@ -179,8 +182,8 @@ var springImageTestCase = struct { { Name: "charsets", Version: "", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar", FileSystemID: "sha256:a1a6ceadb701ab4e6c93b243dc2a0daedc8cee23a24203845ecccd5784cd1393", }), @@ -199,8 +202,8 @@ var springImageTestCase = struct { { Name: "tomcat-embed-el", Version: "9.0.27", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/app/libs/tomcat-embed-el-9.0.27.jar", FileSystemID: "sha256:89504f083d3f15322f97ae240df44650203f24427860db1b3d32e66dd05940e4", }), @@ -218,11 +221,10 @@ var springImageTestCase = struct { }, }, Context: Context{ - Source: &source.Metadata{ - Scheme: source.ImageScheme, - ImageMetadata: source.ImageMetadata{ + Source: &source.Description{ + Metadata: source.StereoscopeImageSourceMetadata{ UserInput: "springio/gs-spring-boot-docker:latest", - Layers: []source.LayerMetadata{ + Layers: []source.StereoscopeLayerMetadata{ { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Digest: "sha256:42a3027eaac150d2b8f516100921f4bd83b3dbc20bfe64124f686c072b49c602", @@ -238,7 +240,6 @@ var springImageTestCase = struct { }, RepoDigests: []string{"springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08"}, }, - Path: "", }, Distro: &linux.Release{ Name: "debian", diff --git a/grype/presenter/config.go b/grype/presenter/config.go deleted file mode 100644 index 44be14aa96c..00000000000 --- a/grype/presenter/config.go +++ /dev/null @@ -1,65 +0,0 @@ -package presenter - -import ( - "errors" - "fmt" - "os" - "text/template" - - presenterTemplate "github.com/anchore/grype/grype/presenter/template" -) - -// Config is the presenter domain's configuration data structure. -type Config struct { - format format - templateFilePath string - showSuppressed bool -} - -// ValidatedConfig returns a new, validated presenter.Config. If a valid Config cannot be created using the given input, -// an error is returned. -func ValidatedConfig(output, outputTemplateFile string, showSuppressed bool) (Config, error) { - format := parse(output) - - if format == unknownFormat { - return Config{}, fmt.Errorf("unsupported output format %q, supported formats are: %+v", output, - AvailableFormats) - } - - if format == templateFormat { - if outputTemplateFile == "" { - return Config{}, fmt.Errorf("must specify path to template file when using %q output format", - templateFormat) - } - - if _, err := os.Stat(outputTemplateFile); errors.Is(err, os.ErrNotExist) { - // file does not exist - return Config{}, fmt.Errorf("template file %q does not exist", - outputTemplateFile) - } - - if _, err := os.ReadFile(outputTemplateFile); err != nil { - return Config{}, fmt.Errorf("unable to read template file: %w", err) - } - - if _, err := template.New("").Funcs(presenterTemplate.FuncMap).ParseFiles(outputTemplateFile); err != nil { - return Config{}, fmt.Errorf("unable to parse template: %w", err) - } - - return Config{ - format: format, - templateFilePath: outputTemplateFile, - }, nil - } - - if outputTemplateFile != "" { - return Config{}, fmt.Errorf("specified template file %q, but "+ - "%q output format must be selected in order to use a template file", - outputTemplateFile, templateFormat) - } - - return Config{ - format: format, - showSuppressed: showSuppressed, - }, nil -} diff --git a/grype/presenter/config_test.go b/grype/presenter/config_test.go deleted file mode 100644 index 3b90686be7b..00000000000 --- a/grype/presenter/config_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package presenter - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestValidatedConfig(t *testing.T) { - cases := []struct { - name string - outputValue string - includeSuppressed bool - outputTemplateFileValue string - expectedConfig Config - assertErrExpectation func(assert.TestingT, error, ...interface{}) bool - }{ - { - "valid template config", - "template", - false, - "./template/test-fixtures/test.valid.template", - Config{ - format: "template", - templateFilePath: "./template/test-fixtures/test.valid.template", - }, - assert.NoError, - }, - { - "template file with non-template format", - "json", - false, - "./some/path/to/a/custom.template", - Config{}, - assert.Error, - }, - { - "unknown format", - "some-made-up-format", - false, - "", - Config{}, - assert.Error, - }, - - { - "table format", - "table", - true, - "", - Config{ - format: tableFormat, - showSuppressed: true, - }, - assert.NoError, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - actualConfig, actualErr := ValidatedConfig(tc.outputValue, tc.outputTemplateFileValue, tc.includeSuppressed) - - assert.Equal(t, tc.expectedConfig, actualConfig) - tc.assertErrExpectation(t, actualErr) - }) - } -} diff --git a/grype/presenter/cyclonedx/presenter.go b/grype/presenter/cyclonedx/presenter.go index 1069d9106a1..ab276d80a92 100644 --- a/grype/presenter/cyclonedx/presenter.go +++ b/grype/presenter/cyclonedx/presenter.go @@ -5,12 +5,11 @@ import ( "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/clio" "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" - "github.com/anchore/grype/internal" - "github.com/anchore/grype/internal/version" "github.com/anchore/syft/syft/formats/common/cyclonedxhelpers" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" @@ -18,9 +17,10 @@ import ( // Presenter writes a CycloneDX report from the given Matches and Scope contents type Presenter struct { + id clio.Identification results match.Matches packages []pkg.Package - srcMetadata *source.Metadata + src *source.Description metadataProvider vulnerability.MetadataProvider format cyclonedx.BOMFileFormat sbom *sbom.SBOM @@ -29,10 +29,11 @@ type Presenter struct { // NewPresenter is a *Presenter constructor func NewJSONPresenter(pb models.PresenterConfig) *Presenter { return &Presenter{ + id: pb.ID, results: pb.Matches, packages: pb.Packages, metadataProvider: pb.MetadataProvider, - srcMetadata: pb.Context.Source, + src: pb.Context.Source, sbom: pb.SBOM, format: cyclonedx.BOMFileFormatJSON, } @@ -41,10 +42,11 @@ func NewJSONPresenter(pb models.PresenterConfig) *Presenter { // NewPresenter is a *Presenter constructor func NewXMLPresenter(pb models.PresenterConfig) *Presenter { return &Presenter{ + id: pb.ID, results: pb.Matches, packages: pb.Packages, metadataProvider: pb.MetadataProvider, - srcMetadata: pb.Context.Source, + src: pb.Context.Source, sbom: pb.SBOM, format: cyclonedx.BOMFileFormatXML, } @@ -57,12 +59,11 @@ func (pres *Presenter) Present(output io.Writer) error { cyclonedxBOM := cyclonedxhelpers.ToFormatModel(*pres.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, + Name: pres.id.Name, + Version: pres.id.Version, }, } diff --git a/grype/presenter/cyclonedx/presenter_test.go b/grype/presenter/cyclonedx/presenter_test.go index bd45a9cc888..5184c8b1495 100644 --- a/grype/presenter/cyclonedx/presenter_test.go +++ b/grype/presenter/cyclonedx/presenter_test.go @@ -7,9 +7,10 @@ import ( "github.com/stretchr/testify/require" + "github.com/anchore/clio" "github.com/anchore/go-testutils" + "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/syft/syft/source" ) var update = flag.Bool("update", false, "update the *.golden files for cyclonedx presenters") @@ -17,9 +18,13 @@ var update = flag.Bool("update", false, "update the *.golden files for cyclonedx func TestCycloneDxPresenterImage(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, source.ImageScheme) - sbom := models.SBOMFromPackages(t, packages) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.ImageSource) + sbom := internal.SBOMFromPackages(t, packages) pb := models.PresenterConfig{ + ID: clio.Identification{ + Name: "grype", + Version: "[not provided]", + }, Matches: matches, Packages: packages, Context: context, @@ -42,17 +47,21 @@ func TestCycloneDxPresenterImage(t *testing.T) { var expected = testutils.GetGoldenFileContents(t) // remove dynamic values, which are tested independently - actual = models.Redact(actual) - expected = models.Redact(expected) + actual = internal.Redact(actual) + expected = internal.Redact(expected) require.JSONEq(t, string(expected), string(actual)) } func TestCycloneDxPresenterDir(t *testing.T) { var buffer bytes.Buffer - matches, packages, ctx, metadataProvider, _, _ := models.GenerateAnalysis(t, source.DirectoryScheme) - sbom := models.SBOMFromPackages(t, packages) + matches, packages, ctx, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.DirectorySource) + sbom := internal.SBOMFromPackages(t, packages) pb := models.PresenterConfig{ + ID: clio.Identification{ + Name: "grype", + Version: "[not provided]", + }, Matches: matches, Packages: packages, Context: ctx, @@ -76,8 +85,8 @@ func TestCycloneDxPresenterDir(t *testing.T) { var expected = testutils.GetGoldenFileContents(t) // remove dynamic values, which are tested independently - actual = models.Redact(actual) - expected = models.Redact(expected) + actual = internal.Redact(actual) + expected = internal.Redact(expected) require.JSONEq(t, string(expected), string(actual)) } diff --git a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden index 9db52f3b476..38d0e4ef8d3 100644 --- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden +++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden @@ -1,11 +1,11 @@ { - "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", "bomFormat": "CycloneDX", - "specVersion": "1.4", - "serialNumber": "urn:uuid:f701dea7-2715-48eb-8d63-878377007e65", + "specVersion": "1.5", + "serialNumber": "urn:uuid:7c707a9a-b23c-45d0-b2e2-229679702a8a", "version": 1, "metadata": { - "timestamp": "2023-05-04T09:41:30-04:00", + "timestamp": "2023-09-20T15:03:10-04:00", "tools": [ { "vendor": "anchore", @@ -52,7 +52,7 @@ ], "vulnerabilities": [ { - "bom-ref": "urn:uuid:befb74e5-738d-4b2c-adf2-03d276553bca", + "bom-ref": "urn:uuid:bdc7d6ad-3d59-4b99-b146-075b10aa8729", "id": "CVE-1999-0001", "source": {}, "references": [ @@ -78,7 +78,7 @@ ] }, { - "bom-ref": "urn:uuid:9cf43de2-c92a-4f29-add6-29bdd71a0285", + "bom-ref": "urn:uuid:90d84886-5bb3-4337-9f40-c4a81e566807", "id": "CVE-1999-0002", "source": {}, "references": [ diff --git a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden index ba5b2c47aaa..26e094e226b 100644 --- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden +++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden @@ -1,11 +1,11 @@ { - "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", "bomFormat": "CycloneDX", - "specVersion": "1.4", - "serialNumber": "urn:uuid:102e3928-5e9e-4352-bdfe-b9eb64b837f8", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c0486275-53fa-4ae3-81c3-71558e96fe56", "version": 1, "metadata": { - "timestamp": "2023-05-04T09:41:30-04:00", + "timestamp": "2023-09-20T15:03:10-04:00", "tools": [ { "vendor": "anchore", @@ -52,7 +52,7 @@ ], "vulnerabilities": [ { - "bom-ref": "urn:uuid:e082487a-f943-4d4a-8f7c-020d4b0838c4", + "bom-ref": "urn:uuid:847eae89-a879-450e-9405-e3827f38c4e8", "id": "CVE-1999-0001", "source": {}, "references": [ @@ -78,7 +78,7 @@ ] }, { - "bom-ref": "urn:uuid:3d8b0870-5c57-4063-b30d-56102dd49ec1", + "bom-ref": "urn:uuid:de9a3c25-e55c-4357-8d0f-b12d31756c30", "id": "CVE-1999-0002", "source": {}, "references": [ 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..92038be7574 --- /dev/null +++ b/grype/presenter/explain/explain.go @@ -0,0 +1,463 @@ +package explain + +import ( + _ "embed" + "fmt" + "io" + "sort" + "strings" + "text/template" + + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "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.Coordinates) explainedEvidence { + path := location.RealPath + if match.Artifact.MetadataType == pkg.JavaMetadataType { + 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..22151b2e66e --- /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": "", + "quiet": false, + "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": { + "structured": false, + "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..661b78b0bab --- /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": "", + "quiet": true, + "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": { + "structured": false, + "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..c14db8cb62b --- /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": "", + "quiet": true, + "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": { + "structured": false, + "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/format.go b/grype/presenter/format.go deleted file mode 100644 index d1aa05803a8..00000000000 --- a/grype/presenter/format.go +++ /dev/null @@ -1,71 +0,0 @@ -package presenter - -import ( - "strings" -) - -const ( - unknownFormat format = "unknown" - jsonFormat format = "json" - tableFormat format = "table" - cycloneDXFormat format = "cyclonedx" - cycloneDXJSON format = "cyclonedx-json" - cycloneDXXML format = "cyclonedx-xml" - sarifFormat format = "sarif" - templateFormat format = "template" - - // DEPRECATED <-- TODO: remove in v1.0 - embeddedVEXJSON format = "embedded-cyclonedx-vex-json" - embeddedVEXXML format = "embedded-cyclonedx-vex-xml" -) - -// format is a dedicated type to represent a specific kind of presenter output format. -type format string - -func (f format) String() string { - return string(f) -} - -// parse returns the presenter.format specified by the given user input. -func parse(userInput string) format { - switch strings.ToLower(userInput) { - case "": - return tableFormat - case strings.ToLower(jsonFormat.String()): - return jsonFormat - case strings.ToLower(tableFormat.String()): - return tableFormat - case strings.ToLower(sarifFormat.String()): - return sarifFormat - case strings.ToLower(templateFormat.String()): - return templateFormat - case strings.ToLower(cycloneDXFormat.String()): - return cycloneDXFormat - case strings.ToLower(cycloneDXJSON.String()): - return cycloneDXJSON - case strings.ToLower(cycloneDXXML.String()): - return cycloneDXXML - case strings.ToLower(embeddedVEXJSON.String()): - return cycloneDXJSON - case strings.ToLower(embeddedVEXXML.String()): - return cycloneDXFormat - default: - return unknownFormat - } -} - -// AvailableFormats is a list of presenter format options available to users. -var AvailableFormats = []format{ - jsonFormat, - tableFormat, - cycloneDXFormat, - cycloneDXJSON, - sarifFormat, - templateFormat, -} - -// DeprecatedFormats TODO: remove in v1.0 -var DeprecatedFormats = []format{ - embeddedVEXJSON, - embeddedVEXXML, -} diff --git a/grype/presenter/internal/test_helpers.go b/grype/presenter/internal/test_helpers.go new file mode 100644 index 00000000000..2ef882518e1 --- /dev/null +++ b/grype/presenter/internal/test_helpers.go @@ -0,0 +1,384 @@ +package internal + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/require" + + 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/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" +) + +const ( + DirectorySource SyftSource = "directory" + ImageSource SyftSource = "image" + FileSource SyftSource = "file" +) + +type SyftSource string + +func GenerateAnalysis(t *testing.T, scheme SyftSource) (match.Matches, []pkg.Package, pkg.Context, vulnerability.MetadataProvider, interface{}, interface{}) { + t.Helper() + + packages := generatePackages(t) + matches := generateMatches(t, packages[0], packages[1]) + context := generateContext(t, scheme) + + return matches, packages, context, models.NewMetadataMock(), nil, nil +} + +func GenerateAnalysisWithIgnoredMatches(t *testing.T, scheme SyftSource) (match.Matches, []match.IgnoredMatch, []pkg.Package, pkg.Context, vulnerability.MetadataProvider, interface{}, interface{}) { + t.Helper() + + packages := generatePackages(t) + matches := generateMatches(t, packages[0], packages[0]) + ignoredMatches := generateIgnoredMatches(t, packages[1]) + context := generateContext(t, scheme) + + return matches, ignoredMatches, packages, context, models.NewMetadataMock(), nil, nil +} + +func SBOMFromPackages(t *testing.T, packages []pkg.Package) *sbom.SBOM { + t.Helper() + + sbom := &sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: syftPkg.NewCollection(), + }, + } + + for _, p := range packages { + sbom.Artifacts.Packages.Add(toSyftPkg(p)) + } + + return sbom +} + +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, + } +} + +func Redact(s []byte) []byte { + serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`) + uuidPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) + 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}`) + + for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, refPattern, uuidPattern, cycloneDxBomRefPattern} { + s = pattern.ReplaceAll(s, []byte("")) + } + return s +} + +func generateMatches(t *testing.T, p, p2 pkg.Package) match.Matches { + t.Helper() + + matches := []match.Match{ + { + + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-1999-0001", + Namespace: "source-1", + Fix: vulnerability.Fix{ + Versions: []string{"the-next-version"}, + State: grypeDb.FixedState, + }, + }, + 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", + }, + }, + }, + }, + { + + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-1999-0002", + Namespace: "source-2", + }, + Package: p2, + Details: []match.Detail{ + { + Type: match.ExactIndirectMatch, + Matcher: match.DpkgMatcher, + SearchedBy: map[string]interface{}{ + "cpe": "somecpe", + }, + Found: map[string]interface{}{ + "constraint": "somecpe", + }, + }, + }, + }, + } + + collection := match.NewMatches(matches...) + + return collection +} + +// nolint: funlen +func generateIgnoredMatches(t *testing.T, p pkg.Package) []match.IgnoredMatch { + t.Helper() + + return []match.IgnoredMatch{ + { + Match: match.Match{ + 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", + }, + }, + Found: map[string]interface{}{ + "constraint": ">= 20", + }, + }, + }, + }, + AppliedIgnoreRules: []match.IgnoreRule{}, + }, + { + Match: match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-1999-0002", + Namespace: "source-2", + }, + 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{}, + }, + { + Match: match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-1999-0004", + Namespace: "source-2", + }, + 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*", + }, + }, + }, + } +} + +func generatePackages(t *testing.T) []pkg.Package { + t.Helper() + epoch := 2 + + pkgs := []pkg.Package{ + { + Name: "package-1", + Version: "1.1.1", + Type: syftPkg.RpmPkg, + 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", + }, + }, + MetadataType: pkg.RpmMetadataType, + Metadata: pkg.RpmMetadata{ + Epoch: &epoch, + }, + }, + { + Name: "package-2", + Version: "2.2.2", + Type: syftPkg.DebPkg, + 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", + }, + }, + Licenses: []string{"MIT", "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) + } + + return updatedPkgs +} + +//nolint:funlen +func generateContext(t *testing.T, scheme SyftSource) pkg.Context { + var ( + src syftSource.Source + desc syftSource.Description + ) + + switch scheme { + case FileSource: + var err error + src, err = syftSource.NewFromFile(syftSource.FileConfig{ + Path: "user-input", + }) + if err != nil { + t.Fatalf("failed to generate mock file source from mock image: %+v", err) + } + desc = src.Describe() + case ImageSource: + img := image.Image{ + Metadata: image.Metadata{ + ID: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", + ManifestDigest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Size: 65, + }, + Layers: []*image.Layer{ + { + Metadata: image.LayerMetadata{ + Digest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 22, + }, + }, + { + Metadata: image.LayerMetadata{ + Digest: "sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006", + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 16, + }, + }, + { + Metadata: image.LayerMetadata{ + Digest: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 27, + }, + }, + }, + } + + 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) + } + 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, + }) + + 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 { + m.Path = "/some/path" + desc.Metadata = m + } + default: + t.Fatalf("unknown scheme: %s", scheme) + } + + return pkg.Context{ + Source: &desc, + Distro: &linux.Release{ + Name: "centos", + IDLike: []string{ + "centos", + }, + Version: "8.0", + }, + } +} diff --git a/grype/presenter/json/presenter.go b/grype/presenter/json/presenter.go index 8b26a35f895..4ad1b262229 100644 --- a/grype/presenter/json/presenter.go +++ b/grype/presenter/json/presenter.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io" + "github.com/anchore/clio" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" @@ -12,6 +13,7 @@ import ( // Presenter is a generic struct for holding fields needed for reporting type Presenter struct { + id clio.Identification matches match.Matches ignoredMatches []match.IgnoredMatch packages []pkg.Package @@ -24,6 +26,7 @@ type Presenter struct { // NewPresenter creates a new JSON presenter func NewPresenter(pb models.PresenterConfig) *Presenter { return &Presenter{ + id: pb.ID, matches: pb.Matches, ignoredMatches: pb.IgnoredMatches, packages: pb.Packages, @@ -36,7 +39,7 @@ func NewPresenter(pb models.PresenterConfig) *Presenter { // 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, + doc, err := models.NewDocument(pres.id, pres.packages, pres.context, pres.matches, pres.ignoredMatches, pres.metadataProvider, pres.appConfig, pres.dbStatus) if err != nil { return err diff --git a/grype/presenter/json/presenter_test.go b/grype/presenter/json/presenter_test.go index 12a471c1fc1..9b2add4108f 100644 --- a/grype/presenter/json/presenter_test.go +++ b/grype/presenter/json/presenter_test.go @@ -4,13 +4,16 @@ import ( "bytes" "flag" "regexp" + "sort" "testing" "github.com/stretchr/testify/assert" + "github.com/anchore/clio" "github.com/anchore/go-testutils" "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" @@ -21,9 +24,13 @@ var timestampRegexp = regexp.MustCompile(`"timestamp":\s*"[^"]+"`) func TestJsonImgsPresenter(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.ImageSource) pb := models.PresenterConfig{ + ID: clio.Identification{ + Name: "grype", + Version: "[not provided]", + }, Matches: matches, Packages: packages, Context: context, @@ -54,9 +61,13 @@ func TestJsonImgsPresenter(t *testing.T) { func TestJsonDirsPresenter(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, source.DirectoryScheme) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.DirectorySource) pb := models.PresenterConfig{ + ID: clio.Identification{ + Name: "grype", + Version: "[not provided]", + }, Matches: matches, Packages: packages, Context: context, @@ -91,7 +102,7 @@ func TestEmptyJsonPresenter(t *testing.T) { matches := match.NewMatches() ctx := pkg.Context{ - Source: &source.Metadata{}, + Source: &source.Description{}, Distro: &linux.Release{ ID: "centos", IDLike: []string{"rhel"}, @@ -100,6 +111,10 @@ func TestEmptyJsonPresenter(t *testing.T) { } pb := models.PresenterConfig{ + ID: clio.Identification{ + Name: "grype", + Version: "[not provided]", + }, Matches: matches, Packages: nil, Context: ctx, @@ -125,6 +140,18 @@ func TestEmptyJsonPresenter(t *testing.T) { } +func TestPresenter_Present_NewDocumentSorted(t *testing.T) { + matches, packages, context, metadataProvider, appConfig, dbStatus := internal.GenerateAnalysis(t, internal.ImageSource) + doc, err := models.NewDocument(clio.Identification{}, packages, context, matches, nil, metadataProvider, appConfig, dbStatus) + if err != nil { + t.Fatal(err) + } + + if !sort.IsSorted(models.MatchSort(doc.Matches)) { + t.Errorf("expected matches to be sorted") + } +} + func redact(content []byte) []byte { return timestampRegexp.ReplaceAll(content, []byte(`"timestamp":""`)) } diff --git a/grype/presenter/models/cvss.go b/grype/presenter/models/cvss.go index 1c65f993361..582901a1a1a 100644 --- a/grype/presenter/models/cvss.go +++ b/grype/presenter/models/cvss.go @@ -3,6 +3,8 @@ package models import "github.com/anchore/grype/grype/vulnerability" type Cvss struct { + Source string `json:"source,omitempty"` + Type string `json:"type,omitempty"` Version string `json:"version"` Vector string `json:"vector"` Metrics CvssMetrics `json:"metrics"` @@ -23,6 +25,8 @@ func NewCVSS(metadata *vulnerability.Metadata) []Cvss { vendorMetadata = make(map[string]interface{}) } cvss = append(cvss, Cvss{ + Source: score.Source, + Type: score.Type, Version: score.Version, Vector: score.Vector, Metrics: CvssMetrics{ diff --git a/grype/presenter/models/document.go b/grype/presenter/models/document.go index 13100da14af..f800f372190 100644 --- a/grype/presenter/models/document.go +++ b/grype/presenter/models/document.go @@ -2,13 +2,13 @@ package models import ( "fmt" + "sort" "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 +21,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 interface{}, dbStatus interface{}) (Document, error) { timestamp, timestampErr := time.Now().Local().MarshalText() if timestampErr != nil { return Document{}, timestampErr @@ -43,6 +43,8 @@ func NewDocument(packages []pkg.Package, context pkg.Context, matches match.Matc findings = append(findings, *matchModel) } + sort.Sort(MatchSort(findings)) + var src *source if context.Source != nil { theSrc, err := newSource(*context.Source) @@ -77,8 +79,8 @@ 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, + Name: id.Name, + Version: id.Version, Configuration: appConfig, VulnerabilityDBStatus: dbStatus, Timestamp: string(timestamp), diff --git a/grype/presenter/models/document_test.go b/grype/presenter/models/document_test.go index dc0031f4881..9c9836177bc 100644 --- a/grype/presenter/models/document_test.go +++ b/grype/presenter/models/document_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/anchore/clio" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" @@ -72,9 +73,8 @@ func TestPackagesAreSorted(t *testing.T) { packages := []pkg.Package{pkg1, pkg2} ctx := pkg.Context{ - Source: &syftSource.Metadata{ - Scheme: syftSource.DirectoryScheme, - ImageMetadata: syftSource.ImageMetadata{}, + Source: &syftSource.Description{ + Metadata: syftSource.DirectorySourceMetadata{}, }, Distro: &linux.Release{ ID: "centos", @@ -82,7 +82,7 @@ func TestPackagesAreSorted(t *testing.T) { 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) if err != nil { t.Fatalf("unable to get document: %+v", err) } @@ -92,7 +92,7 @@ func TestPackagesAreSorted(t *testing.T) { actualVulnerabilities = append(actualVulnerabilities, m.Vulnerability.ID) } - assert.Equal(t, []string{"CVE-1999-0001", "CVE-1999-0002", "CVE-1999-0003"}, actualVulnerabilities) + assert.Equal(t, []string{"CVE-1999-0003", "CVE-1999-0002", "CVE-1999-0001"}, actualVulnerabilities) } func TestTimestampValidFormat(t *testing.T) { @@ -104,7 +104,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) 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..b85465de8b3 100644 --- a/grype/presenter/models/ignore.go +++ b/grype/presenter/models/ignore.go @@ -8,9 +8,11 @@ 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"` + FixState string `json:"fix-state,omitempty"` + Package *IgnoreRulePackage `json:"package,omitempty"` + VexStatus string `json:"vex-status,omitempty"` + VexJustification string `json:"vex-justification,omitempty"` } type IgnoreRulePackage struct { @@ -34,9 +36,11 @@ func newIgnoreRule(r match.IgnoreRule) IgnoreRule { } return IgnoreRule{ - Vulnerability: r.Vulnerability, - FixState: r.FixState, - Package: ignoreRulePackage, + Vulnerability: r.Vulnerability, + FixState: r.FixState, + Package: ignoreRulePackage, + VexStatus: r.VexStatus, + VexJustification: r.VexJustification, } } diff --git a/grype/presenter/models/match.go b/grype/presenter/models/match.go index 10ec2934248..333c8047070 100644 --- a/grype/presenter/models/match.go +++ b/grype/presenter/models/match.go @@ -21,8 +21,8 @@ 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. } func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.MetadataProvider) (*Match, error) { @@ -60,30 +60,36 @@ func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.Metad }, nil } -var _ sort.Interface = (*ByName)(nil) +var _ sort.Interface = (*MatchSort)(nil) -type ByName []Match +type MatchSort []Match // Len is the number of elements in the collection. -func (m ByName) Len() int { +func (m MatchSort) Len() int { return len(m) } // 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 +// sort should consistent across presenters: name, version, type, severity, vulnerability +func (m MatchSort) Less(i, j int) bool { + matchI := m[i] + matchJ := m[j] + if matchI.Artifact.Name == matchJ.Artifact.Name { + if matchI.Artifact.Version == matchJ.Artifact.Version { + if matchI.Artifact.Type == matchJ.Artifact.Type { + if SeverityScore(matchI.Vulnerability.Severity) == SeverityScore(matchJ.Vulnerability.Severity) { + return matchI.Vulnerability.ID > matchJ.Vulnerability.ID + } + return SeverityScore(matchI.Vulnerability.Severity) > SeverityScore(matchJ.Vulnerability.Severity) } - return m[i].Artifact.Version < m[j].Artifact.Version + return matchI.Artifact.Type < matchJ.Artifact.Type } - return m[i].Vulnerability.ID < m[j].Vulnerability.ID + return matchI.Artifact.Version < matchJ.Artifact.Version } - return m[i].Artifact.Name < m[j].Artifact.Name + return matchI.Artifact.Name < matchJ.Artifact.Name } // Swap swaps the elements with indexes i and j. -func (m ByName) Swap(i, j int) { +func (m MatchSort) Swap(i, j int) { m[i], m[j] = m[j], m[i] } diff --git a/grype/presenter/models/metadata_mock.go b/grype/presenter/models/metadata_mock.go index 326fec60f9e..cade2230f02 100644 --- a/grype/presenter/models/metadata_mock.go +++ b/grype/presenter/models/metadata_mock.go @@ -60,6 +60,27 @@ func NewMetadataMock() *MetadataMock { Severity: "High", }, }, + "CVE-1999-0004": { + "source-2": { + Description: "1999-04 description", + Severity: "Critical", + Cvss: []vulnerability.Cvss{ + { + Metrics: vulnerability.NewCvssMetrics( + 1, + 2, + 3, + ), + Vector: "vector", + Version: "2.0", + VendorMetadata: MockVendorMetadata{ + BaseSeverity: "Low", + Status: "verified", + }, + }, + }, + }, + }, }, } } diff --git a/grype/presenter/models/models_helpers.go b/grype/presenter/models/models_helpers.go deleted file mode 100644 index 085c25af4e9..00000000000 --- a/grype/presenter/models/models_helpers.go +++ /dev/null @@ -1,252 +0,0 @@ -package models - -import ( - "regexp" - "testing" - - "github.com/stretchr/testify/require" - - 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/vulnerability" - "github.com/anchore/stereoscope/pkg/image" - "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/cpe" - "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" -) - -func GenerateAnalysis(t *testing.T, scheme syftSource.Scheme) (match.Matches, []pkg.Package, pkg.Context, vulnerability.MetadataProvider, interface{}, interface{}) { - t.Helper() - - packages := generatePackages(t) - matches := generateMatches(t, packages[0], packages[1]) - context := generateContext(t, scheme) - - return matches, packages, context, NewMetadataMock(), nil, nil -} - -func SBOMFromPackages(t *testing.T, packages []pkg.Package) *sbom.SBOM { - t.Helper() - - sbom := &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: syftPkg.NewCollection(), - }, - } - - for _, p := range packages { - sbom.Artifacts.Packages.Add(toSyftPkg(p)) - } - - return sbom -} - -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, - } -} - -func Redact(s []byte) []byte { - serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`) - uuidPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) - 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}`) - - for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, refPattern, uuidPattern, cycloneDxBomRefPattern} { - s = pattern.ReplaceAll(s, []byte("")) - } - return s -} - -func generateMatches(t *testing.T, p, p2 pkg.Package) match.Matches { - t.Helper() - - matches := []match.Match{ - { - - Vulnerability: vulnerability.Vulnerability{ - ID: "CVE-1999-0001", - Namespace: "source-1", - Fix: vulnerability.Fix{ - Versions: []string{"the-next-version"}, - State: grypeDb.FixedState, - }, - }, - 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", - }, - }, - }, - }, - { - - Vulnerability: vulnerability.Vulnerability{ - ID: "CVE-1999-0002", - Namespace: "source-2", - }, - Package: p2, - Details: []match.Detail{ - { - Type: match.ExactIndirectMatch, - Matcher: match.DpkgMatcher, - SearchedBy: map[string]interface{}{ - "cpe": "somecpe", - }, - Found: map[string]interface{}{ - "constraint": "somecpe", - }, - }, - }, - }, - } - - collection := match.NewMatches(matches...) - - return collection -} - -func generatePackages(t *testing.T) []pkg.Package { - t.Helper() - epoch := 2 - - pkgs := []pkg.Package{ - { - Name: "package-1", - Version: "1.1.1", - Type: syftPkg.RpmPkg, - Locations: syftSource.NewLocationSet(syftSource.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", - }, - }, - MetadataType: pkg.RpmMetadataType, - Metadata: pkg.RpmMetadata{ - Epoch: &epoch, - }, - }, - { - Name: "package-2", - Version: "2.2.2", - Type: syftPkg.DebPkg, - Locations: syftSource.NewLocationSet(syftSource.NewVirtualLocation("/foo/bar/somefile-2.txt", "somefile-2.txt")), - CPEs: []cpe.CPE{ - { - Part: "a", - Vendor: "anchore", - Product: "engine", - Version: "2.2.2", - Language: "python", - }, - }, - Licenses: []string{"MIT", "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) - } - - return updatedPkgs -} - -func generateContext(t *testing.T, scheme syftSource.Scheme) pkg.Context { - var src syftSource.Source - img := image.Image{ - Metadata: image.Metadata{ - ID: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", - ManifestDigest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", - MediaType: "application/vnd.docker.distribution.manifest.v2+json", - Size: 65, - }, - Layers: []*image.Layer{ - { - Metadata: image.LayerMetadata{ - Digest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Size: 22, - }, - }, - { - Metadata: image.LayerMetadata{ - Digest: "sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006", - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Size: 16, - }, - }, - { - Metadata: image.LayerMetadata{ - Digest: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Size: 27, - }, - }, - }, - } - - switch scheme { - case syftSource.ImageScheme: - var err error - src, err = syftSource.NewFromImage(&img, "user-input") - if err != nil { - t.Fatalf("failed to generate mock image source from mock image: %+v", err) - } - case syftSource.DirectoryScheme: - var err error - src, err = syftSource.NewFromDirectory("/some/path") - if err != nil { - t.Fatalf("failed to generate mock directory source from mock dir: %+v", err) - } - default: - t.Fatalf("unknown scheme: %s", scheme) - } - - return pkg.Context{ - Source: &src.Metadata, - Distro: &linux.Release{ - Name: "centos", - IDLike: []string{ - "centos", - }, - Version: "8.0", - }, - } -} diff --git a/grype/presenter/models/package.go b/grype/presenter/models/package.go index 770571caa8a..43e0c1aa6e9 100644 --- a/grype/presenter/models/package.go +++ b/grype/presenter/models/package.go @@ -2,24 +2,24 @@ package models import ( "github.com/anchore/grype/grype/pkg" + "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" - syftSource "github.com/anchore/syft/syft/source" ) // 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 []syftSource.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.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"` } type UpstreamPackage struct { @@ -38,7 +38,7 @@ func newPackage(p pkg.Package) Package { licenses = make([]string, 0) } - var coordinates = make([]syftSource.Coordinates, 0) + var coordinates = make([]file.Coordinates, 0) locations := p.Locations.ToSlice() for _, l := range locations { coordinates = append(coordinates, l.Coordinates) diff --git a/grype/presenter/models/presenter_bundle.go b/grype/presenter/models/presenter_bundle.go index dc2c384cfda..d9e9dfd2475 100644 --- a/grype/presenter/models/presenter_bundle.go +++ b/grype/presenter/models/presenter_bundle.go @@ -1,6 +1,7 @@ package models import ( + "github.com/anchore/clio" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" @@ -8,6 +9,7 @@ import ( ) type PresenterConfig struct { + ID clio.Identification Matches match.Matches IgnoredMatches []match.IgnoredMatch Packages []pkg.Package diff --git a/grype/presenter/models/source.go b/grype/presenter/models/source.go index 8648770c40d..bdfecbe3c52 100644 --- a/grype/presenter/models/source.go +++ b/grype/presenter/models/source.go @@ -12,39 +12,38 @@ type source struct { } // newSource creates a new source object to be represented into JSON. -func newSource(src syftSource.Metadata) (source, error) { - switch src.Scheme { - case syftSource.ImageScheme: - metadata := src.ImageMetadata +func newSource(src syftSource.Description) (source, error) { + switch m := src.Metadata.(type) { + case syftSource.StereoscopeImageSourceMetadata: // ensure that empty collections are not shown as null - if metadata.RepoDigests == nil { - metadata.RepoDigests = []string{} + if m.RepoDigests == nil { + m.RepoDigests = []string{} } - if metadata.Tags == nil { - metadata.Tags = []string{} + if m.Tags == nil { + m.Tags = []string{} } return source{ Type: "image", - Target: metadata, + Target: m, }, nil - case syftSource.DirectoryScheme: + case syftSource.DirectorySourceMetadata: return source{ Type: "directory", - Target: src.Path, + Target: m.Path, }, nil - case syftSource.FileScheme: + case syftSource.FileSourceMetadata: return source{ Type: "file", - Target: src.Path, + Target: m.Path, }, nil - case "": + case nil: // we may be showing results from a input source that does not support source information return source{ Type: "unknown", Target: "unknown", }, nil default: - return source{}, fmt.Errorf("unsupported source: %q", src.Scheme) + return source{}, fmt.Errorf("unsupported source: %T", src.Metadata) } } diff --git a/grype/presenter/models/source_test.go b/grype/presenter/models/source_test.go index b1e33a5b132..fc24bef2b26 100644 --- a/grype/presenter/models/source_test.go +++ b/grype/presenter/models/source_test.go @@ -12,14 +12,13 @@ import ( func TestNewSource(t *testing.T) { testCases := []struct { name string - metadata syftSource.Metadata + metadata syftSource.Description expected source }{ { name: "image", - metadata: syftSource.Metadata{ - Scheme: syftSource.ImageScheme, - ImageMetadata: syftSource.ImageMetadata{ + metadata: syftSource.Description{ + Metadata: syftSource.StereoscopeImageSourceMetadata{ UserInput: "abc", ID: "def", ManifestDigest: "abcdef", @@ -28,7 +27,7 @@ func TestNewSource(t *testing.T) { }, expected: source{ Type: "image", - Target: syftSource.ImageMetadata{ + Target: syftSource.StereoscopeImageSourceMetadata{ UserInput: "abc", ID: "def", ManifestDigest: "abcdef", @@ -40,9 +39,10 @@ func TestNewSource(t *testing.T) { }, { name: "directory", - metadata: syftSource.Metadata{ - Scheme: syftSource.DirectoryScheme, - Path: "/foo/bar", + metadata: syftSource.Description{ + Metadata: syftSource.DirectorySourceMetadata{ + Path: "/foo/bar", + }, }, expected: source{ Type: "directory", @@ -51,9 +51,10 @@ func TestNewSource(t *testing.T) { }, { name: "file", - metadata: syftSource.Metadata{ - Scheme: syftSource.FileScheme, - Path: "/foo/bar/test.zip", + metadata: syftSource.Description{ + Metadata: syftSource.FileSourceMetadata{ + Path: "/foo/bar/test.zip", + }, }, expected: source{ Type: "file", @@ -62,18 +63,12 @@ func TestNewSource(t *testing.T) { }, } - var testedSchemes []syftSource.Scheme - for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual, err := newSource(testCase.metadata) require.NoError(t, err) assert.Equal(t, testCase.expected, actual) - testedSchemes = append(testedSchemes, testCase.metadata.Scheme) }) } - - // Ensure we have test coverage for all possible syftSource.Scheme values. - assert.ElementsMatchf(t, syftSource.AllSchemes, testedSchemes, "not all scheme values are being tested") } diff --git a/grype/presenter/models/vulnerability_metadata.go b/grype/presenter/models/vulnerability_metadata.go index b7993fd5807..ced69aea63d 100644 --- a/grype/presenter/models/vulnerability_metadata.go +++ b/grype/presenter/models/vulnerability_metadata.go @@ -35,3 +35,23 @@ func NewVulnerabilityMetadata(id, namespace string, metadata *vulnerability.Meta Cvss: NewCVSS(metadata), } } + +// returns severtiy score for presenter sorting purposes +func SeverityScore(severtiy string) int { + switch severtiy { + case "Unknown": + return 0 + case "Negligible": + return 1 + case "Low": + return 2 + case "Medium": + return 3 + case "High": + return 4 + case "Critical": + return 5 + default: + return 0 + } +} diff --git a/grype/presenter/presenter.go b/grype/presenter/presenter.go index 3ca03e42cc2..72f7a80899c 100644 --- a/grype/presenter/presenter.go +++ b/grype/presenter/presenter.go @@ -1,55 +1,17 @@ package presenter import ( - "io" + "github.com/wagoodman/go-presenter" - "github.com/anchore/grype/grype/presenter/cyclonedx" - "github.com/anchore/grype/grype/presenter/json" "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/grype/grype/presenter/sarif" - "github.com/anchore/grype/grype/presenter/table" - "github.com/anchore/grype/grype/presenter/template" - "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/internal/format" ) -// Presenter is the main interface other Presenters need to implement -type Presenter interface { - Present(io.Writer) error -} - -// GetPresenter retrieves a Presenter that matches a CLI option -// TODO dependency cycle with presenter package to sub formats -func GetPresenter(c Config, pb models.PresenterConfig) Presenter { - switch c.format { - case jsonFormat: - return json.NewPresenter(pb) - case tableFormat: - if c.showSuppressed { - return table.NewPresenter(pb) - } - return table.NewPresenter(pb) - - // NOTE: cyclonedx is identical to embeddedVEXJSON - // The cyclonedx library only provides two BOM formats: JSON and XML - // These embedded formats will be removed in v1.0 - case cycloneDXFormat: - return cyclonedx.NewXMLPresenter(pb) - case cycloneDXJSON: - return cyclonedx.NewJSONPresenter(pb) - case cycloneDXXML: - return cyclonedx.NewXMLPresenter(pb) - case sarifFormat: - return sarif.NewPresenter(pb) - case templateFormat: - return template.NewPresenter(pb, c.templateFilePath) - // DEPRECATED TODO: remove in v1.0 - case embeddedVEXJSON: - log.Warn("embedded-cyclonedx-vex-json format is deprecated and will be removed in v1.0") - return cyclonedx.NewJSONPresenter(pb) - case embeddedVEXXML: - log.Warn("embedded-cyclonedx-vex-xml format is deprecated and will be removed in v1.0") - return cyclonedx.NewXMLPresenter(pb) - default: - return nil - } +// GetPresenter retrieves a Presenter that matches a CLI option. +// Deprecated: this will be removed in v1.0 +func GetPresenter(f string, templatePath string, showSuppressed bool, pb models.PresenterConfig) presenter.Presenter { + return format.GetPresenter(format.Parse(f), format.PresentationConfig{ + TemplateFilePath: templatePath, + ShowSuppressed: showSuppressed, + }, pb) } diff --git a/grype/presenter/sarif/presenter.go b/grype/presenter/sarif/presenter.go index 9288cb4daf3..f54f0514d40 100644 --- a/grype/presenter/sarif/presenter.go +++ b/grype/presenter/sarif/presenter.go @@ -8,30 +8,33 @@ import ( "github.com/owenrumney/go-sarif/sarif" + "github.com/anchore/clio" v5 "github.com/anchore/grype/grype/db/v5" "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" - "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 { + id clio.Identification results match.Matches packages []pkg.Package - srcMetadata *source.Metadata + src *source.Description metadataProvider vulnerability.MetadataProvider } // NewPresenter is a *Presenter constructor func NewPresenter(pb models.PresenterConfig) *Presenter { return &Presenter{ + id: pb.ID, results: pb.Matches, packages: pb.Packages, metadataProvider: pb.MetadataProvider, - srcMetadata: pb.Context.Source, + src: pb.Context.Source, } } @@ -52,8 +55,8 @@ func (pres *Presenter) toSarifReport() (*sarif.Report, error) { return nil, err } - v := version.FromBuild().Version - if v == "[not provided]" { + v := pres.id.Version + if v == "[not provided]" || v == "" { // Need a semver to pass the MS SARIF validator v = "0.0.0-dev" } @@ -61,7 +64,7 @@ func (pres *Presenter) toSarifReport() (*sarif.Report, error) { doc.AddRun(&sarif.Run{ Tool: sarif.Tool{ Driver: &sarif.ToolComponent{ - Name: "Grype", + Name: pres.id.Name, Version: sp(v), InformationURI: sp("https://github.com/anchore/grype"), Rules: pres.sarifRules(), @@ -162,10 +165,19 @@ func (pres *Presenter) packagePath(p pkg.Package) string { // inputPath returns a friendlier relative path or absolute path depending on the input, not prefixed by . or ./ func (pres *Presenter) inputPath() string { - if pres.srcMetadata == nil { + if pres.src == nil { return "" } - inputPath := strings.TrimPrefix(pres.srcMetadata.Path, "./") + var inputPath string + switch m := pres.src.Metadata.(type) { + case source.FileSourceMetadata: + inputPath = m.Path + case source.DirectorySourceMetadata: + inputPath = m.Path + default: + return "" + } + inputPath = strings.TrimPrefix(inputPath, "./") if inputPath == "." { return "" } @@ -173,7 +185,7 @@ func (pres *Presenter) inputPath() string { } // locationPath returns a path for the location, relative to the cwd -func (pres *Presenter) locationPath(l source.Location) string { +func (pres *Presenter) locationPath(l file.Location) string { path := l.RealPath if l.VirtualPath != "" { path = l.VirtualPath @@ -181,13 +193,17 @@ func (pres *Presenter) locationPath(l source.Location) string { in := pres.inputPath() path = strings.TrimPrefix(path, "./") // trimmed off any ./ and accounted for dir:. for both path and input path - if pres.srcMetadata != nil && pres.srcMetadata.Scheme == source.DirectoryScheme { - if filepath.IsAbs(path) || in == "" { - return 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) } - // return a path relative to the cwd, if it's not absolute - return fmt.Sprintf("%s/%s", in, path) } + return path } @@ -197,9 +213,9 @@ func (pres *Presenter) locations(m match.Match) []*sarif.Location { var logicalLocations []*sarif.LogicalLocation - switch pres.srcMetadata.Scheme { - case source.ImageScheme: - img := pres.srcMetadata.ImageMetadata.UserInput + switch metadata := pres.src.Metadata.(type) { + case source.StereoscopeImageSourceMetadata: + img := metadata.UserInput locations := m.Package.Locations.ToSlice() for _, l := range locations { trimmedPath := strings.TrimPrefix(pres.locationPath(l), "/") @@ -214,15 +230,15 @@ func (pres *Presenter) locations(m match.Match) []*sarif.Location { // 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.FileScheme: + case source.FileSourceMetadata: locations := m.Package.Locations.ToSlice() for _, l := range locations { logicalLocations = append(logicalLocations, &sarif.LogicalLocation{ - FullyQualifiedName: sp(fmt.Sprintf("%s:/%s", pres.srcMetadata.Path, pres.locationPath(l))), + FullyQualifiedName: sp(fmt.Sprintf("%s:/%s", metadata.Path, pres.locationPath(l))), Name: sp(l.RealPath), }) } - case source.DirectoryScheme: + case source.DirectorySourceMetadata: // DirectoryScheme is already handled, with input prepended if needed } @@ -398,7 +414,7 @@ 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 pres.srcMetadata.Scheme == source.DirectoryScheme { + 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) diff --git a/grype/presenter/sarif/presenter_test.go b/grype/presenter/sarif/presenter_test.go index a83fbc7c1e8..ffb2c9b1cd1 100644 --- a/grype/presenter/sarif/presenter_test.go +++ b/grype/presenter/sarif/presenter_test.go @@ -8,27 +8,30 @@ import ( "github.com/stretchr/testify/assert" + "github.com/anchore/clio" "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" ) -var update = flag.Bool("update", false, "update .golden files for sarif presenters") +var updateSnapshot = flag.Bool("update-sarif", false, "update .golden files for sarif presenters") func TestSarifPresenter(t *testing.T) { tests := []struct { name string - scheme source.Scheme + scheme internal.SyftSource }{ { name: "directory", - scheme: source.DirectoryScheme, + scheme: internal.DirectorySource, }, { name: "image", - scheme: source.ImageScheme, + scheme: internal.ImageSource, }, } @@ -36,9 +39,12 @@ func TestSarifPresenter(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, tc.scheme) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, tc.scheme) pb := models.PresenterConfig{ + ID: clio.Identification{ + Name: "grype", + }, Matches: matches, Packages: packages, Context: context, @@ -52,13 +58,13 @@ func TestSarifPresenter(t *testing.T) { } actual := buffer.Bytes() - if *update { + if *updateSnapshot { testutils.UpdateGoldenFileContents(t, actual) } var expected = testutils.GetGoldenFileContents(t) - actual = models.Redact(actual) - expected = models.Redact(expected) + actual = internal.Redact(actual) + expected = internal.Redact(expected) if !bytes.Equal(expected, actual) { assert.JSONEq(t, string(expected), string(actual)) @@ -70,83 +76,92 @@ func TestSarifPresenter(t *testing.T) { func Test_locationPath(t *testing.T) { tests := []struct { name string - path string - scheme source.Scheme + metadata any real string virtual string expected string }{ { - name: "dir:.", - scheme: source.DirectoryScheme, - path: ".", + name: "dir:.", + metadata: source.DirectorySourceMetadata{ + Path: ".", + }, real: "/home/usr/file", virtual: "file", expected: "file", }, { - name: "dir:./", - scheme: source.DirectoryScheme, - path: "./", + name: "dir:./", + metadata: source.DirectorySourceMetadata{ + Path: "./", + }, real: "/home/usr/file", virtual: "file", expected: "file", }, { - name: "dir:./someplace", - scheme: source.DirectoryScheme, - path: "./someplace", + name: "dir:./someplace", + metadata: source.DirectorySourceMetadata{ + Path: "./someplace", + }, real: "/home/usr/file", virtual: "file", expected: "someplace/file", }, { - name: "dir:/someplace", - scheme: source.DirectoryScheme, - path: "/someplace", + name: "dir:/someplace", + metadata: source.DirectorySourceMetadata{ + Path: "/someplace", + }, real: "file", expected: "/someplace/file", }, { - name: "dir:/someplace symlink", - scheme: source.DirectoryScheme, - path: "/someplace", + name: "dir:/someplace symlink", + metadata: source.DirectorySourceMetadata{ + Path: "/someplace", + }, real: "/someplace/usr/file", virtual: "file", expected: "/someplace/file", }, { - name: "dir:/someplace absolute", - scheme: source.DirectoryScheme, - path: "/someplace", + name: "dir:/someplace absolute", + metadata: source.DirectorySourceMetadata{ + Path: "/someplace", + }, real: "/usr/file", expected: "/usr/file", }, { - name: "file:/someplace/file", - scheme: source.FileScheme, - path: "/someplace/file", + name: "file:/someplace/file", + metadata: source.FileSourceMetadata{ + Path: "/someplace/file", + }, real: "/usr/file", expected: "/usr/file", }, { - name: "file:/someplace/file relative", - scheme: source.FileScheme, - path: "/someplace/file", + name: "file:/someplace/file relative", + metadata: source.FileSourceMetadata{ + Path: "/someplace/file", + }, real: "file", expected: "file", }, { - name: "image", - scheme: source.ImageScheme, - path: "alpine:latest", + name: "image", + metadata: source.StereoscopeImageSourceMetadata{ + UserInput: "alpine:latest", + }, real: "/etc/file", expected: "/etc/file", }, { - name: "image symlink", - scheme: source.ImageScheme, - path: "alpine:latest", + name: "image symlink", + metadata: source.StereoscopeImageSourceMetadata{ + UserInput: "alpine:latest", + }, real: "/etc/elsewhere/file", virtual: "/etc/file", expected: "/etc/file", @@ -155,15 +170,14 @@ func Test_locationPath(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - pres := createDirPresenter(t, test.path) - pres.srcMetadata = &source.Metadata{ - Scheme: test.scheme, - Path: test.path, + pres := createDirPresenter(t) + pres.src = &source.Description{ + Metadata: test.metadata, } path := pres.packagePath(pkg.Package{ - Locations: source.NewLocationSet( - source.NewVirtualLocation(test.real, test.virtual), + Locations: file.NewLocationSet( + file.NewVirtualLocation(test.real, test.virtual), ), }) @@ -172,19 +186,21 @@ func Test_locationPath(t *testing.T) { } } -func createDirPresenter(t *testing.T, path string) *Presenter { - matches, packages, _, metadataProvider, _, _ := models.GenerateAnalysis(t, source.DirectoryScheme) - s, err := source.NewFromDirectory(path) +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}) if err != nil { t.Fatal(err) } + desc := s.Describe() pb := models.PresenterConfig{ Matches: matches, Packages: packages, MetadataProvider: metadataProvider, Context: pkg.Context{ - Source: &s.Metadata, + Source: &desc, }, } @@ -196,12 +212,12 @@ func createDirPresenter(t *testing.T, path string) *Presenter { func TestToSarifReport(t *testing.T) { tt := []struct { name string - scheme source.Scheme + scheme internal.SyftSource locations map[string]string }{ { name: "directory", - scheme: source.DirectoryScheme, + scheme: internal.DirectorySource, locations: map[string]string{ "CVE-1999-0001-package-1": "/some/path/somefile-1.txt", "CVE-1999-0002-package-2": "/some/path/somefile-2.txt", @@ -209,7 +225,7 @@ func TestToSarifReport(t *testing.T) { }, { name: "image", - scheme: source.ImageScheme, + 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", @@ -222,7 +238,7 @@ func TestToSarifReport(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, tc.scheme) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, tc.scheme) pb := models.PresenterConfig{ Matches: matches, diff --git a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_directory.golden index c8bb7eca8c4..b76b533733b 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": [ diff --git a/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden b/grype/presenter/sarif/test-fixtures/snapshot/TestSarifPresenter_image.golden index 488d7a9ab19..da0565640f0 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": [ diff --git a/grype/presenter/table/__snapshots__/presenter_test.snap b/grype/presenter/table/__snapshots__/presenter_test.snap new file mode 100755 index 00000000000..11c2b0a8ba9 --- /dev/null +++ b/grype/presenter/table/__snapshots__/presenter_test.snap @@ -0,0 +1,29 @@ + +[TestTablePresenter - 1] +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 + +--- + +[TestEmptyTablePresenter - 1] +No vulnerabilities found + +--- + +[TestHidesIgnoredMatches - 1] +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 + +--- + +[TestDisplaysIgnoredMatches - 1] +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-0004 Critical (suppressed by VEX) +package-2 2.2.2 deb CVE-1999-0002 Critical (suppressed) +package-2 2.2.2 deb CVE-1999-0001 Low (suppressed) + +--- diff --git a/grype/presenter/table/presenter.go b/grype/presenter/table/presenter.go index d48e9bf80d8..4c0a4d3cfea 100644 --- a/grype/presenter/table/presenter.go +++ b/grype/presenter/table/presenter.go @@ -16,7 +16,8 @@ import ( ) const ( - appendSuppressed = " (suppressed)" + appendSuppressed = " (suppressed)" + appendSuppressedVEX = " (suppressed by VEX)" ) // Presenter is a generic struct for holding fields needed for reporting @@ -25,15 +26,17 @@ type Presenter struct { ignoredMatches []match.IgnoredMatch packages []pkg.Package metadataProvider vulnerability.MetadataProvider + showSuppressed bool } // NewPresenter is a *Presenter constructor -func NewPresenter(pb models.PresenterConfig) *Presenter { +func NewPresenter(pb models.PresenterConfig, showSuppressed bool) *Presenter { return &Presenter{ results: pb.Matches, ignoredMatches: pb.IgnoredMatches, packages: pb.Packages, metadataProvider: pb.MetadataProvider, + showSuppressed: showSuppressed, } } @@ -45,7 +48,6 @@ func (pres *Presenter) Present(output io.Writer) error { // Generate rows for matching vulnerabilities for m := range pres.results.Enumerate() { row, err := createRow(m, pres.metadataProvider, "") - if err != nil { return err } @@ -53,13 +55,23 @@ func (pres *Presenter) Present(output io.Writer) error { } // Generate rows for suppressed vulnerabilities - for _, m := range pres.ignoredMatches { - row, err := createRow(m.Match, pres.metadataProvider, appendSuppressed) + if pres.showSuppressed { + for _, m := range pres.ignoredMatches { + msg := appendSuppressed + if m.AppliedIgnoreRules != nil { + for i := range m.AppliedIgnoreRules { + if m.AppliedIgnoreRules[i].Namespace == "vex" { + msg = appendSuppressedVEX + } + } + } + row, err := createRow(m.Match, pres.metadataProvider, msg) - if err != nil { - return err + if err != nil { + return err + } + rows = append(rows, row) } - rows = append(rows, row) } if len(rows) == 0 { @@ -67,19 +79,9 @@ func (pres *Presenter) Present(output io.Writer) error { 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) + rows = sortRows(removeDuplicateRows(rows)) table := tablewriter.NewWriter(output) - table.SetHeader(columns) table.SetAutoWrapText(false) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) @@ -100,6 +102,39 @@ func (pres *Presenter) Present(output io.Writer) error { return nil } +func sortRows(rows [][]string) [][]string { + // sort + sort.SliceStable(rows, func(i, j int) bool { + var ( + name = 0 + ver = 1 + packageType = 3 + vuln = 4 + sev = 5 + ) + // name, version, type, severity, vulnerability + // > is for numeric sorting like severity or year/number of vulnerability + // < is for alphabetical sorting like name, version, type + if rows[i][name] == rows[j][name] { + if rows[i][ver] == rows[j][ver] { + if rows[i][packageType] == rows[j][packageType] { + if models.SeverityScore(rows[i][sev]) == models.SeverityScore(rows[j][sev]) { + // we use > here to get the most recently filed vulnerabilities + // to show at the top of the severity + return rows[i][vuln] > rows[j][vuln] + } + return models.SeverityScore(rows[i][sev]) > models.SeverityScore(rows[j][sev]) + } + return rows[i][packageType] < rows[j][packageType] + } + return rows[i][ver] < rows[j][ver] + } + return rows[i][name] < rows[j][name] + }) + + return rows +} + func removeDuplicateRows(items [][]string) [][]string { seen := map[string][]string{} var result [][]string diff --git a/grype/presenter/table/presenter_test.go b/grype/presenter/table/presenter_test.go index bd9dd1a9a2c..c2c7e789893 100644 --- a/grype/presenter/table/presenter_test.go +++ b/grype/presenter/table/presenter_test.go @@ -2,24 +2,22 @@ package table import ( "bytes" - "flag" "testing" + "github.com/gkampitakis/go-snaps/snaps" "github.com/go-test/deep" - "github.com/sergi/go-diff/diffmatchpatch" + "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/match" "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" syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" ) -var update = flag.Bool("update", false, "update the *.golden files for table presenters") - func TestCreateRow(t *testing.T) { pkg1 := pkg.Package{ ID: "package-1-id", @@ -74,9 +72,8 @@ func TestCreateRow(t *testing.T) { } func TestTablePresenter(t *testing.T) { - var buffer bytes.Buffer - matches, packages, _, metadataProvider, _, _ := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, _, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.ImageSource) pb := models.PresenterConfig{ Matches: matches, @@ -84,25 +81,14 @@ func TestTablePresenter(t *testing.T) { MetadataProvider: metadataProvider, } - pres := NewPresenter(pb) + 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) + require.NoError(t, err) - 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) // TODO: add me back in when there is a JSON schema // validateAgainstDbSchema(t, string(actual)) @@ -121,26 +107,14 @@ func TestEmptyTablePresenter(t *testing.T) { MetadataProvider: nil, } - pres := NewPresenter(pb) + 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) { @@ -169,5 +143,74 @@ func TestRemoveDuplicateRows(t *testing.T) { t.Errorf(" diff: %+v", d) } } +} + +func TestSortRows(t *testing.T) { + data := [][]string{ + {"a", "v0.1.0", "", "deb", "CVE-2019-9996", "Critical"}, + {"a", "v0.1.0", "", "deb", "CVE-2018-9996", "Critical"}, + {"a", "v0.2.0", "", "deb", "CVE-2010-9996", "High"}, + {"b", "v0.2.0", "", "deb", "CVE-2010-9996", "Medium"}, + {"b", "v0.2.0", "", "deb", "CVE-2019-9996", "High"}, + {"d", "v0.4.0", "", "node", "CVE-2011-9996", "Low"}, + {"d", "v0.4.0", "", "node", "CVE-2012-9996", "Negligible"}, + {"c", "v0.6.0", "", "node", "CVE-2013-9996", "Critical"}, + } + + expected := [][]string{ + {"a", "v0.1.0", "", "deb", "CVE-2019-9996", "Critical"}, + {"a", "v0.1.0", "", "deb", "CVE-2018-9996", "Critical"}, + {"a", "v0.2.0", "", "deb", "CVE-2010-9996", "High"}, + {"b", "v0.2.0", "", "deb", "CVE-2019-9996", "High"}, + {"b", "v0.2.0", "", "deb", "CVE-2010-9996", "Medium"}, + {"c", "v0.6.0", "", "node", "CVE-2013-9996", "Critical"}, + {"d", "v0.4.0", "", "node", "CVE-2011-9996", "Low"}, + {"d", "v0.4.0", "", "node", "CVE-2012-9996", "Negligible"}, + } + + actual := sortRows(data) + + if diff := cmp.Diff(expected, actual); diff != "" { + t.Errorf("sortRows() mismatch (-want +got):\n%s", diff) + } +} + +func TestHidesIgnoredMatches(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, + } + + pres := NewPresenter(pb, false) + + err := pres.Present(&buffer) + require.NoError(t, err) + + actual := buffer.String() + snaps.MatchSnapshot(t, actual) +} + +func TestDisplaysIgnoredMatches(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, + } + + pres := NewPresenter(pb, true) + + err := pres.Present(&buffer) + require.NoError(t, err) + actual := buffer.String() + snaps.MatchSnapshot(t, actual) } 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/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/template/presenter.go b/grype/presenter/template/presenter.go index 710e2851add..d6fe7a31c78 100644 --- a/grype/presenter/template/presenter.go +++ b/grype/presenter/template/presenter.go @@ -11,6 +11,7 @@ import ( "github.com/Masterminds/sprig/v3" "github.com/mitchellh/go-homedir" + "github.com/anchore/clio" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" @@ -19,6 +20,7 @@ import ( // Presenter is an implementation of presenter.Presenter that formats output according to a user-provided Go text template. type Presenter struct { + id clio.Identification matches match.Matches ignoredMatches []match.IgnoredMatch packages []pkg.Package @@ -32,6 +34,7 @@ type Presenter struct { // NewPresenter returns a new template.Presenter. func NewPresenter(pb models.PresenterConfig, templateFile string) *Presenter { return &Presenter{ + id: pb.ID, matches: pb.Matches, ignoredMatches: pb.IgnoredMatches, packages: pb.Packages, @@ -61,7 +64,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, + document, err := models.NewDocument(pres.id, pres.packages, pres.context, pres.matches, pres.ignoredMatches, pres.metadataProvider, pres.appConfig, pres.dbStatus) if err != nil { return err @@ -91,7 +94,7 @@ var FuncMap = func() template.FuncMap { return collection } - sort.Sort(models.ByName(matches)) + sort.Sort(models.MatchSort(matches)) return matches } return f diff --git a/grype/presenter/template/presenter_test.go b/grype/presenter/template/presenter_test.go index 9acaf6cd59e..502f8ca3f75 100644 --- a/grype/presenter/template/presenter_test.go +++ b/grype/presenter/template/presenter_test.go @@ -11,14 +11,14 @@ import ( "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/source" ) 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 := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, context, metadataProvider, appConfig, dbStatus := internal.GenerateAnalysis(t, internal.ImageSource) workingDirectory, err := os.Getwd() if err != nil { @@ -53,7 +53,7 @@ func TestPresenter_Present(t *testing.T) { } func TestPresenter_SprigDate_Fails(t *testing.T) { - matches, packages, context, metadataProvider, appConfig, dbStatus := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, context, metadataProvider, appConfig, dbStatus := internal.GenerateAnalysis(t, internal.ImageSource) workingDirectory, err := os.Getwd() require.NoError(t, err) diff --git a/grype/search/cpe.go b/grype/search/cpe.go index b962598d6c9..d9d97dd0b6a 100644 --- a/grype/search/cpe.go +++ b/grype/search/cpe.go @@ -17,9 +17,15 @@ import ( syftPkg "github.com/anchore/syft/syft/pkg" ) +type CPEPackageParameter struct { + Name string `json:"name"` + Version string `json:"version"` +} + type CPEParameters struct { Namespace string `json:"namespace"` CPEs []string `json:"cpes"` + Package CPEPackageParameter } func (i *CPEParameters) Merge(other CPEParameters) error { @@ -147,6 +153,10 @@ func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vu CPEs: []string{ searchedByCPE.BindToFmtString(), }, + Package: CPEPackageParameter{ + Name: p.Name, + Version: p.Version, + }, }, Found: CPEResult{ VulnerabilityID: vuln.ID, diff --git a/grype/search/cpe_test.go b/grype/search/cpe_test.go index 70ead138ffe..9a87c407710 100644 --- a/grype/search/cpe_test.go +++ b/grype/search/cpe_test.go @@ -102,7 +102,7 @@ func (pr *mockVulnStore) stub() { { PackageName: "funfun", VersionConstraint: "= 5.2.1", - VersionFormat: version.PythonFormat.String(), + VersionFormat: version.UnknownFormat.String(), ID: "CVE-2017-fake-6", CPEs: []string{ "cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*", @@ -197,6 +197,10 @@ func TestFindMatchesByPackageCPE(t *testing.T) { 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:*:*"}, @@ -247,6 +251,10 @@ func TestFindMatchesByPackageCPE(t *testing.T) { "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:*:*"}, @@ -280,6 +288,10 @@ func TestFindMatchesByPackageCPE(t *testing.T) { 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:*:*"}, @@ -325,6 +337,10 @@ func TestFindMatchesByPackageCPE(t *testing.T) { 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:*:*:*:*:*:*:*"}, @@ -378,6 +394,10 @@ func TestFindMatchesByPackageCPE(t *testing.T) { 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:*:*:*:*:*:*:*:*"}, @@ -424,6 +444,10 @@ func TestFindMatchesByPackageCPE(t *testing.T) { 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{ @@ -484,6 +508,10 @@ func TestFindMatchesByPackageCPE(t *testing.T) { SearchedBy: CPEParameters{ CPEs: []string{"cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", + Package: CPEPackageParameter{ + Name: "sw", + Version: "0.1", + }, }, Found: CPEResult{ CPEs: []string{ @@ -536,13 +564,17 @@ func TestFindMatchesByPackageCPE(t *testing.T) { 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)", + VersionConstraint: "= 5.2.1 (unknown)", VulnerabilityID: "CVE-2017-fake-6", }, Matcher: matcher, @@ -583,6 +615,10 @@ func TestFindMatchesByPackageCPE(t *testing.T) { SearchedBy: CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", + Package: CPEPackageParameter{ + Name: "handlebars", + Version: "0.1", + }, }, Found: CPEResult{ CPEs: []string{ @@ -629,6 +665,10 @@ func TestFindMatchesByPackageCPE(t *testing.T) { SearchedBy: CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", + Package: CPEPackageParameter{ + Name: "handlebars", + Version: "0.1", + }, }, Found: CPEResult{ CPEs: []string{ diff --git a/grype/search/language.go b/grype/search/language.go index 8b0f154a657..57612c45dac 100644 --- a/grype/search/language.go +++ b/grype/search/language.go @@ -46,6 +46,10 @@ func ByPackageLanguage(store vulnerability.ProviderByLanguage, d *distro.Distro, 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, diff --git a/grype/search/language_test.go b/grype/search/language_test.go index 8e8f59351bd..9c610a73df9 100644 --- a/grype/search/language_test.go +++ b/grype/search/language_test.go @@ -79,6 +79,7 @@ func expectedMatch(p pkg.Package, constraint string) []match.Match { 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, diff --git a/grype/version/constraint.go b/grype/version/constraint.go index 5f9b5b7ab18..773a472511a 100644 --- a/grype/version/constraint.go +++ b/grype/version/constraint.go @@ -20,12 +20,7 @@ func GetConstraint(constStr string, format Format) (Constraint, error) { 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: diff --git a/grype/version/constraint_unit.go b/grype/version/constraint_unit.go index d02b4211492..23aef540ea5 100644 --- a/grype/version/constraint_unit.go +++ b/grype/version/constraint_unit.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" ) // operator group only matches on range operators (GT, LT, GTE, LTE, E) @@ -19,7 +19,7 @@ type constraintUnit struct { } func parseUnit(phrase string) (*constraintUnit, error) { - match := internal.MatchCaptureGroups(constraintPartPattern, phrase) + match := stringutil.MatchCaptureGroups(constraintPartPattern, phrase) version, exists := match["version"] if !exists { return nil, nil diff --git a/grype/version/fuzzy_constraint.go b/grype/version/fuzzy_constraint.go index abda758d501..77d788332cf 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 diff --git a/grype/version/fuzzy_constraint_test.go b/grype/version/fuzzy_constraint_test.go index 150a383e30e..d28584f07d5 100644 --- a/grype/version/fuzzy_constraint_test.go +++ b/grype/version/fuzzy_constraint_test.go @@ -264,6 +264,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 +371,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/pep440_constraint.go b/grype/version/pep440_constraint.go new file mode 100644 index 00000000000..2aa39caae97 --- /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, fmt.Errorf("(python) unsupported format: %s", 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 constrain 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..422b6190282 --- /dev/null +++ b/grype/version/pep440_version.go @@ -0,0 +1,34 @@ +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) { + if other.Format != PythonFormat { + return -1, fmt.Errorf("unable to compare pep440 to given format: %s", other.Format) + } + 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/portage_version.go b/grype/version/portage_version.go index 4b1fa950437..48575e24872 100644 --- a/grype/version/portage_version.go +++ b/grype/version/portage_version.go @@ -175,7 +175,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..344a91b173c 100644 --- a/grype/version/portage_version_test.go +++ b/grype/version/portage_version_test.go @@ -18,6 +18,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 { 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/version.go b/grype/version/version.go index b11cda901c9..f2404bef69d 100644 --- a/grype/version/version.go +++ b/grype/version/version.go @@ -14,13 +14,14 @@ 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 + rpmVer *rpmVersion + kbVer *kbVersion + portVer *portageVersion + pep440version *pep440Version } func NewVersion(raw string, format Format) (*Version, error) { @@ -66,8 +67,9 @@ func (v *Version) populate() error { 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 diff --git a/grype/vex/openvex/implementation.go b/grype/vex/openvex/implementation.go new file mode 100644 index 00000000000..fa20ce75222 --- /dev/null +++ b/grype/vex/openvex/implementation.go @@ -0,0 +1,324 @@ +package openvex + +import ( + "errors" + "fmt" + "net/url" + "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.StereoscopeImageSourceMetadata: + // TODO(puerco): We can create a wider definition here. This effectively + // adds the multiarch image and the image of the OS running grype. We + // could generate more identifiers to match better. + return identifiersFromDigests(v.RepoDigests), nil + default: + // Fail for now + return nil, errors.New("source type not supported for VEX") + } +} + +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 digestString, repoURL string + shaString := ref.Identifier() + + // If not a digest, we can't form a purl, so skip it + if !strings.HasPrefix(shaString, "sha256:") { + continue + } + + digestString = url.QueryEscape(shaString) + + 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, digestString, 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..6407df46e24 --- /dev/null +++ b/grype/vex/openvex/implementation_test.go @@ -0,0 +1,38 @@ +package openvex + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +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/library", + "124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + }, + }, + { + "cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc", + []string{ + "cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc", + "pkg:oci/curl@sha256%3A9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc?repository_url=cgr.dev/chainguard", + "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..85168f2b1ab --- /dev/null +++ b/grype/vex/processor_test.go @@ -0,0 +1,314 @@ +package vex + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + v5 "github.com/anchore/grype/grype/db/v5" + "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.StereoscopeImageSourceMetadata{ + 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{ + ID: "CVE-2023-3817", + Namespace: "alpine:distro:alpine:3.17", + Fix: vulnerability.Fix{ + Versions: []string{"3.0.10-r0"}, + State: v5.FixedState, + }, + }, + Package: libCryptoPackage, + } + + libCryptoCVE_2023_1255 := match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-2023-1255", + Namespace: "alpine:distro:alpine:3.17", + Fix: vulnerability.Fix{ + Versions: []string{"3.0.8-r4"}, + State: v5.FixedState, + }, + }, + Package: libCryptoPackage, + } + + libCryptoCVE_2023_2975 := match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-2023-2975", + Namespace: "alpine:distro:alpine:3.17", + Fix: vulnerability.Fix{ + Versions: []string{"3.0.9-r2"}, + State: v5.FixedState, + }, + }, + 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 + } + + metchesRef := 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: metchesRef(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: metchesRef(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: metchesRef(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: metchesRef(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: metchesRef(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: metchesRef(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-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..3cb81469aae 100644 --- a/grype/vulnerability/fix.go +++ b/grype/vulnerability/fix.go @@ -4,6 +4,15 @@ import ( grypeDb "github.com/anchore/grype/grype/db/v5" ) +func AllFixStates() []grypeDb.FixState { + return []grypeDb.FixState{ + grypeDb.FixedState, + grypeDb.NotFixedState, + grypeDb.UnknownFixState, + grypeDb.WontFixState, + } +} + type Fix struct { Versions []string State grypeDb.FixState diff --git a/grype/vulnerability/metadata.go b/grype/vulnerability/metadata.go index 5dbb97b5970..fb87aedb2f2 100644 --- a/grype/vulnerability/metadata.go +++ b/grype/vulnerability/metadata.go @@ -15,6 +15,8 @@ type Metadata struct { } type Cvss struct { + Source string + Type string Version string Vector string Metrics CvssMetrics @@ -47,6 +49,8 @@ func NewCvss(m []grypeDB.Cvss) []Cvss { 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{ diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go index 3eff7f81d25..83c3799cd3c 100644 --- a/grype/vulnerability_matcher.go +++ b/grype/vulnerability_matcher.go @@ -1,15 +1,33 @@ package grype import ( + "fmt" "strings" + "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/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" + "github.com/anchore/syft/syft/linux" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +const ( + branch = "├──" + leaf = "└──" ) type VulnerabilityMatcher struct { @@ -18,6 +36,7 @@ type VulnerabilityMatcher struct { IgnoreRules []match.IgnoreRule FailSeverity *vulnerability.Severity NormalizeByCVE bool + VexProcessor *vex.Processor } func DefaultVulnerabilityMatcher(store store.Store) *VulnerabilityMatcher { @@ -42,9 +61,39 @@ 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.SetCompleted() + if err != nil { + progressMonitor.MatchesDiscovered.SetError(err) + } + }() + + remainingMatches, ignoredMatches, err = m.findDBMatches(pkgs, context, progressMonitor) + if err != nil { + 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 + } + + 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 := m.searchDBForMatches(context.Distro, pkgs, progressMonitor) matches, ignoredMatches = m.applyIgnoreRules(matches) @@ -69,6 +118,85 @@ func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Conte return &matches, ignoredMatches, err } +func (m *VulnerabilityMatcher) searchDBForMatches( + release *linux.Release, + packages []pkg.Package, + progressMonitor *monitorWriter, +) match.Matches { + var err error + res := match.NewMatches() + matcherIndex, defaultMatcher := newMatcherIndex(m.Matchers) + + 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.NewMatches() + } + } + + if defaultMatcher == nil { + defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true}) + } + for _, p := range packages { + progressMonitor.PackagesProcessed.Increment() + log.WithFields("package", displayPackage(p)).Trace("searching for vulnerability matches") + + matchAgainst, ok := matcherIndex[p.Type] + if !ok { + matchAgainst = []matcher.Matcher{defaultMatcher} + } + for _, theMatcher := range matchAgainst { + matches, err := theMatcher.Match(m.Store, d, p) + if err != nil { + log.WithFields("error", err, "package", displayPackage(p)).Warn("matcher failed") + } else { + // Filter out matches based on records in the database exclusion table and hard-coded rules + filtered, dropped := match.ApplyExplicitIgnoreRules(m.Store, match.NewMatches(matches...)) + + additionalMatches := filtered.Sorted() + logPackageMatches(p, additionalMatches) + logExplicitDroppedPackageMatches(p, dropped) + res.Add(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.Store) + } + } + } + + return res +} + +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) + } + + 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.Store) + + return matchesAfterVex, ignoredMatchesAfterVex, nil +} + func (m *VulnerabilityMatcher) applyIgnoreRules(matches match.Matches) (match.Matches, []match.IgnoredMatch) { var ignoredMatches []match.IgnoredMatch if len(m.IgnoreRules) == 0 { @@ -98,12 +226,19 @@ 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 } @@ -111,7 +246,7 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { upstreamMetadata, err := m.Store.GetMetadata(ref.ID, ref.Namespace) 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,6 +266,53 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { return match } +func displayPackage(p pkg.Package) string { + if p.PURL != "" { + return p.PURL + } + return fmt.Sprintf("%s@%s (%s)", p.Name, p.Version, p.Type) +} + +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.Match.Fingerprint()] = struct{}{} + } + + var diff []match.IgnoredMatch + for _, b := range subject { + if _, ok := otherMap[b.Match.Fingerprint()]; !ok { + diff = append(diff, b) + } + } + + return diff +} + +func newMatcherIndex(matchers []matcher.Matcher) (map[syftPkg.Type][]matcher.Matcher, matcher.Matcher) { + matcherIndex := make(map[syftPkg.Type][]matcher.Matcher) + var defaultMatcher matcher.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.Matcher, 0) + } + + matcherIndex[t] = append(matcherIndex[t], m) + log.Debugf("adding matcher: %+v", t) + } + } + + return matcherIndex, defaultMatcher +} + func isCVE(id string) bool { return strings.HasPrefix(strings.ToLower(id), "cve-") } @@ -151,3 +333,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.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace) + 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 == grypeDb.FixedState { + 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 fde08f091ba..0e856170698 100644 --- a/grype/vulnerability_matcher_test.go +++ b/grype/vulnerability_matcher_test.go @@ -491,6 +491,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, + Package: search.CPEPackageParameter{ + Name: "activerecord", + Version: "3.7.5", + }, }, Found: search.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", @@ -526,6 +530,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { 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)", @@ -579,6 +584,20 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { }, 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, + }, { Type: match.CPEMatch, SearchedBy: search.CPEParameters{ @@ -586,6 +605,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, + Package: search.CPEPackageParameter{ + Name: "activerecord", + Version: "3.7.5", + }, }, Found: search.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", @@ -597,19 +620,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { Matcher: "ruby-gem-matcher", Confidence: 0.9, }, - { - Type: match.ExactDirectMatch, - SearchedBy: map[string]any{ - "language": "ruby", - "namespace": "github:language:ruby", - }, - Found: map[string]any{ - "versionConstraint": "< 3.7.6 (unknown)", - "vulnerabilityID": "GHSA-2014-fake-3", - }, - Matcher: "ruby-gem-matcher", - Confidence: 1, - }, }, }, ), @@ -661,6 +671,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, + Package: search.CPEPackageParameter{ + Name: "activerecord", + Version: "3.7.5", + }, }, Found: search.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", @@ -731,6 +745,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { 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)", @@ -788,6 +803,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { 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)", @@ -826,6 +842,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, + Package: search.CPEPackageParameter{ + Name: "activerecord", + Version: "3.7.5", + }, }, Found: search.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", diff --git a/internal/bus/bus.go b/internal/bus/bus.go index fdbca50625e..2ba893dcdc8 100644 --- a/internal/bus/bus.go +++ b/internal/bus/bus.go @@ -3,17 +3,13 @@ package bus import "github.com/wagoodman/go-partybus" var publisher partybus.Publisher -var active bool -func SetPublisher(p partybus.Publisher) { +func Set(p partybus.Publisher) { publisher = p - if p != nil { - active = true - } } func Publish(event partybus.Event) { - if active { + if publisher != nil { publisher.Publish(event) } } diff --git a/internal/bus/helpers.go b/internal/bus/helpers.go new file mode 100644 index 00000000000..9157ad492d5 --- /dev/null +++ b/internal/bus/helpers.go @@ -0,0 +1,31 @@ +package bus + +import ( + "github.com/wagoodman/go-partybus" + + "github.com/anchore/clio" + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/internal/redact" +) + +func Exit() { + Publish(clio.ExitEvent(false)) +} + +func ExitWithInterrupt() { + Publish(clio.ExitEvent(true)) +} + +func Report(report string) { + Publish(partybus.Event{ + Type: event.CLIReport, + Value: redact.Apply(report), + }) +} + +func Notify(message string) { + Publish(partybus.Event{ + Type: event.CLINotification, + Value: message, + }) +} diff --git a/internal/config/application.go b/internal/config/application.go deleted file mode 100644 index b1d2357108d..00000000000 --- a/internal/config/application.go +++ /dev/null @@ -1,250 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "path" - "reflect" - "strings" - - "github.com/adrg/xdg" - "github.com/mitchellh/go-homedir" - "github.com/spf13/viper" - "gopkg.in/yaml.v2" - - "github.com/anchore/go-logger" - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/grype/internal" -) - -var ErrApplicationConfigNotFound = fmt.Errorf("application config not found") - -type defaultValueLoader interface { - loadDefaultValues(*viper.Viper) -} - -type parser interface { - parseConfigValues() error -} - -type Application struct { - ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading) - Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"` - Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting - 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/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/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 index ea1f6b7b8c6..4097fcd87ba 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -1,5 +1,4 @@ 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/file/getter.go b/internal/file/getter.go index 3e312ec50e1..216a0965a70 100644 --- a/internal/file/getter.go +++ b/internal/file/getter.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/go-getter/helper/url" "github.com/wagoodman/go-progress" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" ) var ( @@ -62,7 +62,7 @@ func (g HashiGoGetter) GetToDir(dst, src string, monitors ...*progress.Manual) e func validateHTTPSource(src string) error { // we are ignoring any sources that are not destined to use the http getter object - if !internal.HasAnyOfPrefixes(src, "http://", "https://") { + if !stringutil.HasAnyOfPrefixes(src, "http://", "https://") { return nil } @@ -71,7 +71,7 @@ func validateHTTPSource(src string) error { return fmt.Errorf("bad URL provided %q: %w", src, err) } // only allow for sources with archive extensions - if !internal.HasAnyOfSuffixes(u.Path, archiveExtensions...) { + if !stringutil.HasAnyOfSuffixes(u.Path, archiveExtensions...) { return ErrNonArchiveSource } return nil diff --git a/internal/format/format.go b/internal/format/format.go new file mode 100644 index 00000000000..f6c099b346b --- /dev/null +++ b/internal/format/format.go @@ -0,0 +1,71 @@ +package format + +import ( + "strings" +) + +const ( + UnknownFormat Format = "unknown" + JSONFormat Format = "json" + TableFormat Format = "table" + CycloneDXFormat Format = "cyclonedx" + CycloneDXJSON Format = "cyclonedx-json" + CycloneDXXML Format = "cyclonedx-xml" + SarifFormat Format = "sarif" + TemplateFormat Format = "template" + + // DEPRECATED <-- TODO: remove in v1.0 + EmbeddedVEXJSON Format = "embedded-cyclonedx-vex-json" + EmbeddedVEXXML Format = "embedded-cyclonedx-vex-xml" +) + +// Format is a dedicated type to represent a specific kind of presenter output format. +type Format string + +func (f Format) String() string { + return string(f) +} + +// Parse returns the presenter.format specified by the given user input. +func Parse(userInput string) Format { + switch strings.ToLower(userInput) { + case "": + return TableFormat + case strings.ToLower(JSONFormat.String()): + return JSONFormat + case strings.ToLower(TableFormat.String()): + return TableFormat + case strings.ToLower(SarifFormat.String()): + return SarifFormat + case strings.ToLower(TemplateFormat.String()): + return TemplateFormat + case strings.ToLower(CycloneDXFormat.String()): + return CycloneDXFormat + case strings.ToLower(CycloneDXJSON.String()): + return CycloneDXJSON + case strings.ToLower(CycloneDXXML.String()): + return CycloneDXXML + case strings.ToLower(EmbeddedVEXJSON.String()): + return CycloneDXJSON + case strings.ToLower(EmbeddedVEXXML.String()): + return CycloneDXFormat + default: + return UnknownFormat + } +} + +// AvailableFormats is a list of presenter format options available to users. +var AvailableFormats = []Format{ + JSONFormat, + TableFormat, + CycloneDXFormat, + CycloneDXJSON, + SarifFormat, + TemplateFormat, +} + +// DeprecatedFormats TODO: remove in v1.0 +var DeprecatedFormats = []Format{ + EmbeddedVEXJSON, + EmbeddedVEXXML, +} diff --git a/grype/presenter/format_test.go b/internal/format/format_test.go similarity index 74% rename from grype/presenter/format_test.go rename to internal/format/format_test.go index f26a529747d..665b442b749 100644 --- a/grype/presenter/format_test.go +++ b/internal/format/format_test.go @@ -1,4 +1,4 @@ -package presenter +package format import ( "testing" @@ -9,29 +9,29 @@ import ( func TestParse(t *testing.T) { cases := []struct { input string - expected format + expected Format }{ { "", - tableFormat, + TableFormat, }, { "table", - tableFormat, + TableFormat, }, { "jSOn", - jsonFormat, + JSONFormat, }, { "booboodepoopoo", - unknownFormat, + UnknownFormat, }, } for _, tc := range cases { t.Run(tc.input, func(t *testing.T) { - actual := parse(tc.input) + actual := Parse(tc.input) assert.Equal(t, tc.expected, actual, "unexpected result for input %q", tc.input) }) } diff --git a/internal/format/presenter.go b/internal/format/presenter.go new file mode 100644 index 00000000000..e365eaee587 --- /dev/null +++ b/internal/format/presenter.go @@ -0,0 +1,51 @@ +package format + +import ( + "github.com/wagoodman/go-presenter" + + "github.com/anchore/grype/grype/presenter/cyclonedx" + "github.com/anchore/grype/grype/presenter/json" + "github.com/anchore/grype/grype/presenter/models" + "github.com/anchore/grype/grype/presenter/sarif" + "github.com/anchore/grype/grype/presenter/table" + "github.com/anchore/grype/grype/presenter/template" + "github.com/anchore/grype/internal/log" +) + +type PresentationConfig struct { + TemplateFilePath string + ShowSuppressed bool +} + +// GetPresenter retrieves a Presenter that matches a CLI option +func GetPresenter(format Format, c PresentationConfig, pb models.PresenterConfig) presenter.Presenter { + switch format { + case JSONFormat: + return json.NewPresenter(pb) + case TableFormat: + return table.NewPresenter(pb, c.ShowSuppressed) + + // NOTE: cyclonedx is identical to EmbeddedVEXJSON + // The cyclonedx library only provides two BOM formats: JSON and XML + // These embedded formats will be removed in v1.0 + case CycloneDXFormat: + return cyclonedx.NewXMLPresenter(pb) + case CycloneDXJSON: + return cyclonedx.NewJSONPresenter(pb) + case CycloneDXXML: + return cyclonedx.NewXMLPresenter(pb) + case SarifFormat: + return sarif.NewPresenter(pb) + case TemplateFormat: + return template.NewPresenter(pb, c.TemplateFilePath) + // DEPRECATED TODO: remove in v1.0 + case EmbeddedVEXJSON: + log.Warn("embedded-cyclonedx-vex-json format is deprecated and will be removed in v1.0") + return cyclonedx.NewJSONPresenter(pb) + case EmbeddedVEXXML: + log.Warn("embedded-cyclonedx-vex-xml format is deprecated and will be removed in v1.0") + return cyclonedx.NewXMLPresenter(pb) + default: + return nil + } +} diff --git a/internal/format/writer.go b/internal/format/writer.go new file mode 100644 index 00000000000..feb8f4ecdca --- /dev/null +++ b/internal/format/writer.go @@ -0,0 +1,219 @@ +package format + +import ( + "bytes" + "fmt" + "io" + "os" + "path" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/mitchellh/go-homedir" + + "github.com/anchore/grype/grype/presenter/models" + "github.com/anchore/grype/internal/bus" + "github.com/anchore/grype/internal/log" +) + +type ScanResultWriter interface { + Write(result models.PresenterConfig) error +} + +var _ ScanResultWriter = (*scanResultMultiWriter)(nil) + +var _ interface { + io.Closer + ScanResultWriter +} = (*scanResultStreamWriter)(nil) + +// MakeScanResultWriter creates a ScanResultWriter for output or returns an error. this will either return a valid writer +// or an error but neither both and if there is no error, ScanResultWriter.Close() should be called +func MakeScanResultWriter(outputs []string, defaultFile string, cfg PresentationConfig) (ScanResultWriter, error) { + outputOptions, err := parseOutputFlags(outputs, defaultFile, cfg) + if err != nil { + return nil, err + } + + writer, err := newMultiWriter(outputOptions...) + if err != nil { + return nil, err + } + + return writer, nil +} + +// MakeScanResultWriterForFormat creates a ScanResultWriter for the given format or returns an error. +func MakeScanResultWriterForFormat(f string, path string, cfg PresentationConfig) (ScanResultWriter, error) { + format := Parse(f) + + if format == UnknownFormat { + return nil, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, f, AvailableFormats) + } + + writer, err := newMultiWriter(newWriterDescription(format, path, cfg)) + if err != nil { + return nil, err + } + + return writer, nil +} + +// parseOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file +func parseOutputFlags(outputs []string, defaultFile string, cfg PresentationConfig) (out []scanResultWriterDescription, errs error) { + // always should have one option -- we generally get the default of "table", but just make sure + if len(outputs) == 0 { + outputs = append(outputs, TableFormat.String()) + } + + for _, name := range outputs { + name = strings.TrimSpace(name) + + // split to at most two parts for = + parts := strings.SplitN(name, "=", 2) + + // the format name is the first part + name = parts[0] + + // default to the --file or empty string if not specified + file := defaultFile + + // If a file is specified as part of the output formatName, use that + if len(parts) > 1 { + file = parts[1] + } + + format := Parse(name) + + if format == UnknownFormat { + errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, AvailableFormats)) + continue + } + + out = append(out, newWriterDescription(format, file, cfg)) + } + return out, errs +} + +// scanResultWriterDescription Format and path strings used to create ScanResultWriter +type scanResultWriterDescription struct { + Format Format + Path string + Cfg PresentationConfig +} + +func newWriterDescription(f Format, p string, cfg PresentationConfig) scanResultWriterDescription { + expandedPath, err := homedir.Expand(p) + if err != nil { + log.Warnf("could not expand given writer output path=%q: %w", p, err) + // ignore errors + expandedPath = p + } + return scanResultWriterDescription{ + Format: f, + Path: expandedPath, + Cfg: cfg, + } +} + +// scanResultMultiWriter holds a list of child ScanResultWriters to apply all Write and Close operations to +type scanResultMultiWriter struct { + writers []ScanResultWriter +} + +// newMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used +func newMultiWriter(options ...scanResultWriterDescription) (_ *scanResultMultiWriter, err error) { + if len(options) == 0 { + return nil, fmt.Errorf("no output options provided") + } + + out := &scanResultMultiWriter{} + + for _, option := range options { + switch len(option.Path) { + case 0: + out.writers = append(out.writers, &scanResultPublisher{ + format: option.Format, + cfg: option.Cfg, + }) + default: + // create any missing subdirectories + dir := path.Dir(option.Path) + if dir != "" { + s, err := os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ? + if err != nil { + return nil, err + } + } else if !s.IsDir() { + return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path) + } + } + fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return nil, fmt.Errorf("unable to create report file: %w", err) + } + out.writers = append(out.writers, &scanResultStreamWriter{ + format: option.Format, + out: fileOut, + cfg: option.Cfg, + }) + } + } + + return out, nil +} + +// Write writes the result to all writers +func (m *scanResultMultiWriter) Write(s models.PresenterConfig) (errs error) { + for _, w := range m.writers { + err := w.Write(s) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("unable to write result: %w", err)) + } + } + return errs +} + +// scanResultStreamWriter implements ScanResultWriter for a given format and io.Writer, also providing a close function for cleanup +type scanResultStreamWriter struct { + format Format + cfg PresentationConfig + out io.Writer +} + +// Write the provided result to the data stream +func (w *scanResultStreamWriter) Write(s models.PresenterConfig) error { + pres := GetPresenter(w.format, w.cfg, s) + if err := pres.Present(w.out); err != nil { + return fmt.Errorf("unable to encode result: %w", err) + } + return nil +} + +// Close any resources, such as open files +func (w *scanResultStreamWriter) Close() error { + if closer, ok := w.out.(io.Closer); ok { + return closer.Close() + } + return nil +} + +// scanResultPublisher implements ScanResultWriter that publishes results to the event bus +type scanResultPublisher struct { + format Format + cfg PresentationConfig +} + +// Write the provided result to the data stream +func (w *scanResultPublisher) Write(s models.PresenterConfig) error { + pres := GetPresenter(w.format, w.cfg, s) + buf := &bytes.Buffer{} + if err := pres.Present(buf); err != nil { + return fmt.Errorf("unable to encode result: %w", err) + } + + bus.Report(buf.String()) + return nil +} diff --git a/internal/format/writer_test.go b/internal/format/writer_test.go new file mode 100644 index 00000000000..3d0c1af1f79 --- /dev/null +++ b/internal/format/writer_test.go @@ -0,0 +1,218 @@ +package format + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/docker/docker/pkg/homedir" + "github.com/stretchr/testify/assert" +) + +func Test_MakeScanResultWriter(t *testing.T) { + tests := []struct { + outputs []string + wantErr assert.ErrorAssertionFunc + }{ + { + outputs: []string{"json"}, + wantErr: assert.NoError, + }, + { + outputs: []string{"table", "json"}, + wantErr: assert.NoError, + }, + { + outputs: []string{"unknown"}, + wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool { + return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are: [`) + }, + }, + } + + for _, tt := range tests { + _, err := MakeScanResultWriter(tt.outputs, "", PresentationConfig{}) + tt.wantErr(t, err) + } +} + +func Test_newSBOMMultiWriter(t *testing.T) { + type writerConfig struct { + format string + file string + } + + tmp := t.TempDir() + + testName := func(options []scanResultWriterDescription, err bool) string { + var out []string + for _, opt := range options { + out = append(out, string(opt.Format)+"="+opt.Path) + } + errs := "" + if err { + errs = "(err)" + } + return strings.Join(out, ", ") + errs + } + + tests := []struct { + outputs []scanResultWriterDescription + err bool + expected []writerConfig + }{ + { + outputs: []scanResultWriterDescription{}, + err: true, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "table", + Path: "", + }, + }, + expected: []writerConfig{ + { + format: "table", + }, + }, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "json", + }, + }, + expected: []writerConfig{ + { + format: "json", + }, + }, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "json", + Path: "test-2.json", + }, + }, + expected: []writerConfig{ + { + format: "json", + file: "test-2.json", + }, + }, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "json", + Path: "test-3/1.json", + }, + { + Format: "spdx-json", + Path: "test-3/2.json", + }, + }, + expected: []writerConfig{ + { + format: "json", + file: "test-3/1.json", + }, + { + format: "spdx-json", + file: "test-3/2.json", + }, + }, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "text", + }, + { + Format: "spdx-json", + Path: "test-4.json", + }, + }, + expected: []writerConfig{ + { + format: "text", + }, + { + format: "spdx-json", + file: "test-4.json", + }, + }, + }, + } + + for _, test := range tests { + t.Run(testName(test.outputs, test.err), func(t *testing.T) { + outputs := test.outputs + for i := range outputs { + if outputs[i].Path != "" { + outputs[i].Path = tmp + outputs[i].Path + } + } + + mw, err := newMultiWriter(outputs...) + + if test.err { + assert.Error(t, err) + return + } else { + assert.NoError(t, err) + } + + assert.Len(t, mw.writers, len(test.expected)) + + for i, e := range test.expected { + switch w := mw.writers[i].(type) { + case *scanResultStreamWriter: + assert.Equal(t, string(w.format), e.format) + assert.NotNil(t, w.out) + if e.file != "" { + assert.FileExists(t, tmp+e.file) + } + case *scanResultPublisher: + assert.Equal(t, string(w.format), e.format) + default: + t.Fatalf("unknown writer type: %T", w) + } + + } + }) + } +} + +func Test_newSBOMWriterDescription(t *testing.T) { + tests := []struct { + name string + path string + expected string + }{ + { + name: "expand home dir", + path: "~/place.txt", + expected: filepath.Join(homedir.Get(), "place.txt"), + }, + { + name: "passthrough other paths", + path: "/other/place.txt", + expected: "/other/place.txt", + }, + { + name: "no path", + path: "", + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := newWriterDescription("table", tt.path, PresentationConfig{}) + assert.Equal(t, tt.expected, o.Path) + }) + } +} diff --git a/internal/log/log.go b/internal/log/log.go index 42ef116e2e2..ff3bbb68564 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -1,69 +1,90 @@ +/* +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 +} + +func Get() logger.Logger { + return log +} // Errorf takes a formatted template string and template arguments for the error logging level. func Errorf(format string, args ...interface{}) { - Log.Errorf(format, args...) + log.Errorf(format, args...) } // Error logs the given arguments at the error logging level. func Error(args ...interface{}) { - Log.Error(args...) + log.Error(args...) } // Warnf takes a formatted template string and template arguments for the warning logging level. func Warnf(format string, args ...interface{}) { - Log.Warnf(format, args...) + log.Warnf(format, args...) } // Warn logs the given arguments at the warning logging level. func Warn(args ...interface{}) { - Log.Warn(args...) + log.Warn(args...) } // Infof takes a formatted template string and template arguments for the info logging level. func Infof(format string, args ...interface{}) { - Log.Infof(format, args...) + log.Infof(format, args...) } // Info logs the given arguments at the info logging level. func Info(args ...interface{}) { - Log.Info(args...) + log.Info(args...) } // Debugf takes a formatted template string and template arguments for the debug logging level. func Debugf(format string, args ...interface{}) { - Log.Debugf(format, args...) + log.Debugf(format, args...) } // Debug logs the given arguments at the debug logging level. func Debug(args ...interface{}) { - Log.Debug(args...) + log.Debug(args...) } // Tracef takes a formatted template string and template arguments for the trace logging level. func Tracef(format string, args ...interface{}) { - Log.Tracef(format, args...) + log.Tracef(format, args...) } // Trace logs the given arguments at the trace logging level. func Trace(args ...interface{}) { - Log.Trace(args...) + log.Trace(args...) } // WithFields returns a message logger with multiple key-value fields. func WithFields(fields ...interface{}) logger.MessageLogger { - return Log.WithFields(fields...) + return log.WithFields(fields...) } // Nested returns a new logger with hard coded key-value pairs func Nested(fields ...interface{}) logger.Logger { - return Log.Nested(fields...) + return log.Nested(fields...) } 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/format/color.go b/internal/stringutil/color.go similarity index 93% rename from internal/format/color.go rename to internal/stringutil/color.go index fa1757c3415..373b98e20ea 100644 --- a/internal/format/color.go +++ b/internal/stringutil/color.go @@ -1,4 +1,4 @@ -package format +package stringutil import "fmt" diff --git a/internal/parse.go b/internal/stringutil/parse.go similarity index 95% rename from internal/parse.go rename to internal/stringutil/parse.go index 300825c986e..6b33c718d0f 100644 --- a/internal/parse.go +++ b/internal/stringutil/parse.go @@ -1,4 +1,4 @@ -package internal +package stringutil import "regexp" diff --git a/internal/string_helpers.go b/internal/stringutil/string_helpers.go similarity index 61% rename from internal/string_helpers.go rename to internal/stringutil/string_helpers.go index b29850522c9..25d21f02c5c 100644 --- a/internal/string_helpers.go +++ b/internal/stringutil/string_helpers.go @@ -1,4 +1,4 @@ -package internal +package stringutil import "strings" @@ -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/string_helpers_test.go b/internal/stringutil/string_helpers_test.go similarity index 74% rename from internal/string_helpers_test.go rename to internal/stringutil/string_helpers_test.go index 44fd05aadf2..89baa28f9b1 100644 --- a/internal/string_helpers_test.go +++ b/internal/stringutil/string_helpers_test.go @@ -1,4 +1,4 @@ -package internal +package stringutil import ( "testing" @@ -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/stringset.go b/internal/stringutil/stringset.go similarity index 96% rename from internal/stringset.go rename to internal/stringutil/stringset.go index 41518aaade0..49a73daab22 100644 --- a/internal/stringset.go +++ b/internal/stringutil/stringset.go @@ -1,4 +1,4 @@ -package internal +package stringutil type StringSet map[string]struct{} diff --git a/internal/format/tprint.go b/internal/stringutil/tprint.go similarity index 94% rename from internal/format/tprint.go rename to internal/stringutil/tprint.go index fc75400bc89..8d874f298bf 100644 --- a/internal/format/tprint.go +++ b/internal/stringutil/tprint.go @@ -1,4 +1,4 @@ -package format +package stringutil import ( "bytes" diff --git a/internal/ui/common_event_handlers.go b/internal/ui/common_event_handlers.go deleted file mode 100644 index 126a04fa42d..00000000000 --- a/internal/ui/common_event_handlers.go +++ /dev/null @@ -1,36 +0,0 @@ -package ui - -import ( - "fmt" - "io" - - "github.com/wagoodman/go-partybus" - - grypeEventParsers "github.com/anchore/grype/grype/event/parsers" -) - -func handleVulnerabilityScanningFinished(event partybus.Event, reportOutput io.Writer) error { - // show the report to stdout - pres, err := grypeEventParsers.ParseVulnerabilityScanningFinished(event) - if err != nil { - return fmt.Errorf("bad CatalogerFinished event: %w", err) - } - - if err := pres.Present(reportOutput); err != nil { - return fmt.Errorf("unable to show vulnerability report: %w", err) - } - return nil -} - -func handleNonRootCommandFinished(event partybus.Event, reportOutput io.Writer) error { - // show the report to stdout - result, err := grypeEventParsers.ParseNonRootCommandFinished(event) - if err != nil { - return fmt.Errorf("bad NonRootCommandFinished event: %w", err) - } - - if _, err := reportOutput.Write([]byte(*result)); err != nil { - return fmt.Errorf("unable to show vulnerability report: %w", err) - } - return nil -} diff --git a/internal/ui/components/spinner.go b/internal/ui/components/spinner.go deleted file mode 100644 index 9c93687673e..00000000000 --- a/internal/ui/components/spinner.go +++ /dev/null @@ -1,40 +0,0 @@ -package components - -import ( - "strings" - "sync" -) - -// TODO: move me to a common module (used in multiple repos) - -const SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" - -type Spinner struct { - index int - charset []string - lock sync.Mutex -} - -func NewSpinner(charset string) Spinner { - return Spinner{ - charset: strings.Split(charset, ""), - } -} - -func (s *Spinner) Current() string { - s.lock.Lock() - defer s.lock.Unlock() - - return s.charset[s.index] -} - -func (s *Spinner) Next() string { - s.lock.Lock() - defer s.lock.Unlock() - c := s.charset[s.index] - s.index++ - if s.index >= len(s.charset) { - s.index = 0 - } - return c -} diff --git a/internal/ui/ephemeral_terminal_ui.go b/internal/ui/ephemeral_terminal_ui.go deleted file mode 100644 index 498bebc22e9..00000000000 --- a/internal/ui/ephemeral_terminal_ui.go +++ /dev/null @@ -1,166 +0,0 @@ -//go:build linux || darwin -// +build linux darwin - -package ui - -import ( - "bytes" - "context" - "fmt" - "io" - "os" - "sync" - - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" - - "github.com/anchore/go-logger" - grypeEvent "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/ui" -) - -// ephemeralTerminalUI provides an "ephemeral" terminal user interface to display the application state dynamically. -// The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line -// UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen -// must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make -// a shared state, bytes coming from elsewhere to the screen will disrupt this state. -// -// This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a -// published element on the event bus, typically polling the element for the latest state. This allows for the UI to -// control update frequency to the screen, provide "liveness" indications that are interpolated between bus events, -// and overall loosely couple the bus events from screen interactions. -// -// By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should -// attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by -// convention, each new event that the UI should respond to should be added either in this package as a handler function, -// or in the shared ui package as a function on the main handler object. All handler functions should be completed -// processing an event before the ETUI exits (coordinated with a sync.WaitGroup) -type ephemeralTerminalUI struct { - unsubscribe func() error - handler *ui.Handler - waitGroup *sync.WaitGroup - frame *frame.Frame - logBuffer *bytes.Buffer - uiOutput *os.File - reportOutput io.Writer -} - -// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer. -func NewEphemeralTerminalUI(reportWriter io.Writer) UI { - return &ephemeralTerminalUI{ - handler: ui.NewHandler(), - waitGroup: &sync.WaitGroup{}, - uiOutput: os.Stderr, - reportOutput: reportWriter, - } -} - -func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error { - h.unsubscribe = unsubscribe - hideCursor(h.uiOutput) - - // prep the logger to not clobber the screen from now on (logrus only) - h.logBuffer = bytes.NewBufferString("") - logController, ok := log.Log.(logger.Controller) - if ok { - logController.SetOutput(h.logBuffer) - } - - return h.openScreen() -} - -func (h *ephemeralTerminalUI) Handle(event partybus.Event) error { - ctx := context.Background() - switch { - case h.handler.RespondsTo(event): - if err := h.handler.Handle(ctx, h.frame, event, h.waitGroup); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - case event.Type == grypeEvent.AppUpdateAvailable: - if err := handleAppUpdateAvailable(ctx, h.frame, event, h.waitGroup); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - case event.Type == grypeEvent.VulnerabilityScanningFinished: - // we need to close the screen now since signaling the the presenter is ready means that we - // are about to write bytes to stdout, so we should reset the terminal state first - h.closeScreen(false) - - if err := handleVulnerabilityScanningFinished(event, h.reportOutput); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - // this is the last expected event, stop listening to events - return h.unsubscribe() - - case event.Type == grypeEvent.NonRootCommandFinished: - h.closeScreen(false) - - if err := handleNonRootCommandFinished(event, h.reportOutput); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - // this is the last expected event, stop listening to events - return h.unsubscribe() - } - return nil -} - -func (h *ephemeralTerminalUI) openScreen() error { - config := frame.Config{ - PositionPolicy: frame.PolicyFloatForward, - // only report output to stderr, reserve report output for stdout - Output: h.uiOutput, - } - - fr, err := frame.New(config) - if err != nil { - return fmt.Errorf("failed to create the screen object: %w", err) - } - h.frame = fr - - return nil -} - -func (h *ephemeralTerminalUI) closeScreen(force bool) { - // we may have other background processes still displaying progress, wait for them to - // finish before discontinuing dynamic content and showing the final report - if !h.frame.IsClosed() { - if !force { - h.waitGroup.Wait() - } - h.frame.Close() - // TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output - frame.Close() - - // only flush the log on close - h.flushLog() - } -} - -func (h *ephemeralTerminalUI) flushLog() { - // flush any errors to the screen before the report - logController, ok := log.Log.(logger.Controller) - if ok { - fmt.Fprint(logController.GetOutput(), h.logBuffer.String()) - logController.SetOutput(h.uiOutput) - } else { - fmt.Fprint(h.uiOutput, h.logBuffer.String()) - } -} - -func (h *ephemeralTerminalUI) Teardown(force bool) error { - h.closeScreen(force) - showCursor(h.uiOutput) - return nil -} - -func hideCursor(output io.Writer) { - fmt.Fprint(output, "\x1b[?25l") -} - -func showCursor(output io.Writer) { - fmt.Fprint(output, "\x1b[?25h") -} diff --git a/internal/ui/etui_event_handlers.go b/internal/ui/etui_event_handlers.go deleted file mode 100644 index 09b2f66559b..00000000000 --- a/internal/ui/etui_event_handlers.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build linux || darwin -// +build linux darwin - -package ui - -import ( - "context" - "fmt" - "io" - "sync" - - "github.com/gookit/color" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" - - grypeEventParsers "github.com/anchore/grype/grype/event/parsers" - "github.com/anchore/grype/internal" - "github.com/anchore/grype/internal/version" -) - -func handleAppUpdateAvailable(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error { - newVersion, err := grypeEventParsers.ParseAppUpdateAvailable(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - line, err := fr.Prepend() - if err != nil { - return err - } - - message := color.Magenta.Sprintf("New version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version) - _, _ = io.WriteString(line, message) - - return nil -} diff --git a/internal/ui/logger_ui.go b/internal/ui/logger_ui.go deleted file mode 100644 index f0eed4ada81..00000000000 --- a/internal/ui/logger_ui.go +++ /dev/null @@ -1,50 +0,0 @@ -package ui - -import ( - "io" - - "github.com/wagoodman/go-partybus" - - grypeEvent "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/internal/log" -) - -type loggerUI struct { - unsubscribe func() error - reportOutput io.Writer -} - -// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer. -func NewLoggerUI(reportWriter io.Writer) UI { - return &loggerUI{ - reportOutput: reportWriter, - } -} - -func (l *loggerUI) Setup(unsubscribe func() error) error { - l.unsubscribe = unsubscribe - return nil -} - -func (l loggerUI) Handle(event partybus.Event) error { - switch event.Type { - case grypeEvent.VulnerabilityScanningFinished: - if err := handleVulnerabilityScanningFinished(event, l.reportOutput); err != nil { - log.Warnf("unable to show catalog image finished event: %+v", err) - } - case grypeEvent.NonRootCommandFinished: - if err := handleNonRootCommandFinished(event, l.reportOutput); err != nil { - log.Warnf("unable to show command finished event: %+v", err) - } - // ignore all events except for the final events - default: - return nil - } - - // this is the last expected event, stop listening to events - return l.unsubscribe() -} - -func (l loggerUI) Teardown(_ bool) error { - return nil -} diff --git a/internal/ui/select.go b/internal/ui/select.go deleted file mode 100644 index 2ecd90cfdfb..00000000000 --- a/internal/ui/select.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build linux || darwin -// +build linux darwin - -package ui - -import ( - "io" - "os" - - "golang.org/x/term" -) - -// TODO: build tags to exclude options from windows - -// 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, reportWriter io.Writer) (uis []UI) { - isStdoutATty := term.IsTerminal(int(os.Stdout.Fd())) - isStderrATty := term.IsTerminal(int(os.Stderr.Fd())) - notATerminal := !isStderrATty && !isStdoutATty - - switch { - case verbose || quiet || notATerminal || !isStderrATty: - uis = append(uis, NewLoggerUI(reportWriter)) - default: - uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter)) - } - - return uis -} diff --git a/internal/ui/select_windows.go b/internal/ui/select_windows.go deleted file mode 100644 index 20e7e58f9a6..00000000000 --- a/internal/ui/select_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build windows -// +build windows - -package ui - -import ( - "io" -) - -// 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, reportWriter io.Writer) (uis []UI) { - return append(uis, NewLoggerUI(reportWriter)) -} diff --git a/internal/ui/ui.go b/internal/ui/ui.go deleted file mode 100644 index cb551f1cfcb..00000000000 --- a/internal/ui/ui.go +++ /dev/null @@ -1,11 +0,0 @@ -package ui - -import ( - "github.com/wagoodman/go-partybus" -) - -type UI interface { - Setup(unsubscribe func() error) error - partybus.Handler - Teardown(force bool) error -} 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/main.go b/main.go deleted file mode 100644 index 6378cd3f2b5..00000000000 --- a/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/anchore/grype/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/schema/cyclonedx/Makefile b/schema/cyclonedx/Makefile index c54faff4bf5..2c122004abd 100644 --- a/schema/cyclonedx/Makefile +++ b/schema/cyclonedx/Makefile @@ -5,10 +5,10 @@ validate-schema: validate-schema-xml validate-schema-json .PHONY: validate-schema-xml validate-schema-xml: - go run ../../main.go -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o cyclonedx-xml > bom.xml + go run ../../cmd/grype -c ../../test/grype-test-config.yaml ubuntu:latest -v -o cyclonedx-xml > bom.xml xmllint --noout --schema ./cyclonedx.xsd bom.xml .PHONY: validate-schema-json validate-schema-json: - go run ../../main.go -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o cyclonedx-json > bom.json + go run ../../cmd/grype -c ../../test/grype-test-config.yaml ubuntu:latest -v -o cyclonedx-json > bom.json ../../.tmp/yajsv -s cyclonedx.json bom.json diff --git a/schema/cyclonedx/README.md b/schema/cyclonedx/README.md index beed2007fdd..5f49a3ee7ef 100644 --- a/schema/cyclonedx/README.md +++ b/schema/cyclonedx/README.md @@ -3,3 +3,18 @@ `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. + +## Updating + +You will need to go to https://github.com/CycloneDX/specification/blob/1.5/schema and download the latest `bom-#.#.xsd` and `spdx.xsd`. + +Additionally, for `xmllint` to function you will need to patch the bom schema with the location to the SPDX schema by changing: + +```xml + +``` + +To: +```xml + +``` \ No newline at end of file diff --git a/schema/cyclonedx/cyclonedx.json b/schema/cyclonedx/cyclonedx.json index 627cbc7dba4..eca12983238 100644 --- a/schema/cyclonedx/cyclonedx.json +++ b/schema/cyclonedx/cyclonedx.json @@ -1,1697 +1,3800 @@ { - "$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, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://cyclonedx.org/schema/bom-1.5.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.5.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.5"] + }, + "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'.", + "minimum": 1, + "default": 1, + "examples": [1] + }, + "metadata": { + "$ref": "#/definitions/metadata", + "title": "BOM Metadata", + "description": "Provides additional information about a BOM." + }, + "components": { + "type": "array", + "items": {"$ref": "#/definitions/component"}, + "uniqueItems": true, + "title": "Components", + "description": "A list of software and hardware components." + }, + "services": { + "type": "array", + "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", + "items": {"$ref": "#/definitions/externalReference"}, + "title": "External References", + "description": "External references provide a way to document systems, sites, and information that may be relevant, but are not included with the BOM. They may also establish specific relationships within or external to the BOM." + }, + "dependencies": { + "type": "array", + "items": {"$ref": "#/definitions/dependency"}, + "uniqueItems": true, + "title": "Dependencies", + "description": "Provides the ability to document dependency relationships." + }, + "compositions": { + "type": "array", + "items": {"$ref": "#/definitions/compositions"}, + "uniqueItems": true, + "title": "Compositions", + "description": "Compositions describe constituent parts (including components, services, and dependency relationships) and their completeness. The completeness of vulnerabilities expressed in a BOM may also be described." + }, + "vulnerabilities": { + "type": "array", + "items": {"$ref": "#/definitions/vulnerability"}, + "uniqueItems": true, + "title": "Vulnerabilities", + "description": "Vulnerabilities identified in components or services." + }, + "annotations": { + "type": "array", + "items": {"$ref": "#/definitions/annotations"}, + "uniqueItems": true, + "title": "Annotations", + "description": "Comments made by people, organizations, or tools about any object with a bom-ref, such as components, services, vulnerabilities, or the BOM itself. Unlike inventory information, annotations may contain opinion or commentary from various stakeholders. Annotations may be inline (with inventory) or externalized via BOM-Link, and may optionally be signed." + }, + "formulation": { + "type": "array", + "items": {"$ref": "#/definitions/formula"}, + "uniqueItems": true, + "title": "Formulation", + "description": "Describes how a component or service was manufactured or deployed. This is achieved through the use of formulas, workflows, tasks, and steps, which declare the precise steps to reproduce along with the observed formulas describing the steps which transpired in the manufacturing process." + }, "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)." + "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.", + "items": { + "$ref": "#/definitions/property" } }, - "definitions": { - "refType": { - "$comment": "Identifier-DataType for interlinked elements.", - "type": "string" - }, - "metadata": { - "type": "object", - "title": "BOM Metadata Object", - "additionalProperties": false, + "signature": { + "$ref": "#/definitions/signature", + "title": "Signature", + "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)." + } + }, + "definitions": { + "refType": { + "description": "Identifier for referable and therefore interlink-able elements.", + "type": "string", + "minLength": 1, + "$comment": "value SHOULD not start with the BOM-Link intro 'urn:cdx:'" + }, + "refLinkType": { + "description": "Descriptor for an element identified by the attribute 'bom-ref' in the same BOM document.\nIn contrast to `bomLinkElementType`.", + "allOf": [{"$ref": "#/definitions/refType"}] + }, + "bomLinkDocumentType": { + "title": "BOM-Link Document", + "description": "Descriptor for another BOM document. See https://cyclonedx.org/capabilities/bomlink/", + "type": "string", + "format": "iri-reference", + "pattern": "^urn:cdx:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/[1-9][0-9]*$", + "$comment": "part of the pattern is based on `bom.serialNumber`'s pattern" + }, + "bomLinkElementType": { + "title": "BOM-Link Element", + "description": "Descriptor for an element in a BOM document. See https://cyclonedx.org/capabilities/bomlink/", + "type": "string", + "format": "iri-reference", + "pattern": "^urn:cdx:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/[1-9][0-9]*#.+$", + "$comment": "part of the pattern is based on `bom.serialNumber`'s pattern" + }, + "bomLink": { + "anyOf": [ + { + "title": "BOM-Link Document", + "$ref": "#/definitions/bomLinkDocumentType" + }, + { + "title": "BOM-Link Element", + "$ref": "#/definitions/bomLinkElementType" + } + ] + }, + "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." + }, + "lifecycles": { + "type": "array", + "title": "Lifecycles", + "description": "", + "items": { + "type": "object", + "title": "Lifecycle", + "description": "The product lifecycle(s) that this BOM represents.", + "oneOf": [ + { + "required": ["phase"], + "additionalProperties": false, + "properties": { + "phase": { + "type": "string", + "title": "Phase", + "description": "A pre-defined phase in the product lifecycle.\n\n* __design__ = BOM produced early in the development lifecycle containing inventory of components and services that are proposed or planned to be used. The inventory may need to be procured, retrieved, or resourced prior to use.\n* __pre-build__ = BOM consisting of information obtained prior to a build process and may contain source files and development artifacts and manifests. The inventory may need to be resolved and retrieved prior to use.\n* __build__ = BOM consisting of information obtained during a build process where component inventory is available for use. The precise versions of resolved components are usually available at this time as well as the provenance of where the components were retrieved from.\n* __post-build__ = BOM consisting of information obtained after a build process has completed and the resulting components(s) are available for further analysis. Built components may exist as the result of a CI/CD process, may have been installed or deployed to a system or device, and may need to be retrieved or extracted from the system or device.\n* __operations__ = BOM produced that represents inventory that is running and operational. This may include staging or production environments and will generally encompass multiple SBOMs describing the applications and operating system, along with HBOMs describing the hardware that makes up the system. Operations Bill of Materials (OBOM) can provide full-stack inventory of runtime environments, configurations, and additional dependencies.\n* __discovery__ = BOM consisting of information observed through network discovery providing point-in-time enumeration of embedded, on-premise, and cloud-native services such as server applications, connected devices, microservices, and serverless functions.\n* __decommission__ = BOM containing inventory that will be, or has been retired from operations.", + "enum": [ + "design", + "pre-build", + "build", + "post-build", + "operations", + "discovery", + "decommission" + ] + } + } + }, + { + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the lifecycle phase" + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description of the lifecycle phase" + } + } + } + ] + } + }, + "tools": { + "oneOf": [ + { + "type": "object", + "title": "Creation Tools", + "description": "The tool(s) used in the creation of the BOM.", + "additionalProperties": false, + "properties": { + "components": { + "type": "array", + "items": {"$ref": "#/definitions/component"}, + "uniqueItems": true, + "title": "Components", + "description": "A list of software and hardware components used as tools" + }, + "services": { + "type": "array", + "items": {"$ref": "#/definitions/service"}, + "uniqueItems": true, + "title": "Services", + "description": "A list of services used as tools. This may include microservices, function-as-a-service, and other types of network or intra-process services." + } + } + }, + { + "type": "array", + "title": "Creation Tools (legacy)", + "description": "[Deprecated] The tool(s) used in the creation of the BOM.", + "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.", + "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": { + "title": "BOM License(s)", + "$ref": "#/definitions/licenseChoice" + }, "properties": { - "timestamp": { + "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.", + "items": {"$ref": "#/definitions/property"} + } + } + }, + "tool": { + "type": "object", + "title": "Tool", + "description": "[Deprecated] - DO NOT USE. This will be removed in a future version. This will be removed in a future version. Use component or service instead. 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", + "items": {"$ref": "#/definitions/hash"}, + "title": "Hashes", + "description": "The hashes of the tool (if applicable)." + }, + "externalReferences": { + "type": "array", + "items": {"$ref": "#/definitions/externalReference"}, + "title": "External References", + "description": "External references provide a way to document systems, sites, and information that may be relevant, but are not included with the BOM. They may also establish specific relationships within or external to the BOM." + } + } + }, + "organizationalEntity": { + "type": "object", + "title": "Organizational Entity Object", + "description": "", + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the organization", + "examples": [ + "Example Inc." + ] + }, + "url": { + "type": "array", + "items": { "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"} + "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.", + "items": {"$ref": "#/definitions/organizationalContact"} + } + } + }, + "organizationalContact": { + "type": "object", + "title": "Organizational Contact Object", + "description": "", + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the object elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + }, + "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", + "platform", + "operating-system", + "device", + "device-driver", + "firmware", + "file", + "machine-learning-model", + "data" + ], + "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* __platform__ = A runtime environment which interprets or executes software. This may include runtimes such as those that execute bytecode or low-code/no-code application platforms.\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 See also the list of [known device properties](https://github.com/CycloneDX/cyclonedx-property-taxonomy/blob/main/cdx/device.md).\n* __device-driver__ = A special type of software that operates or controls a particular type of device. Refer to [https://en.wikipedia.org/wiki/Device_driver](https://en.wikipedia.org/wiki/Device_driver)\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.\n* __machine-learning-model__ = A model based on training data that can make predictions or decisions without being explicitly programmed to do so.\n* __data__ = A collection of discrete values that convey information.", + "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", + "items": {"$ref": "#/definitions/hash"} + }, + "licenses": { + "$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": { - "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"} + "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.", + "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.", + "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.", + "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.", + "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.", + "items": {"$ref": "#/definitions/patch"} + }, + "notes": { + "type": "string", + "title": "Notes", + "description": "Notes, observations, and other non-structured commentary describing the components pedigree." + } } + }, + "externalReferences": { + "type": "array", + "items": {"$ref": "#/definitions/externalReference"}, + "title": "External References", + "description": "External references provide a way to document systems, sites, and information that may be relevant, but are not included with the BOM. They may also establish specific relationships within or external to the BOM." + }, + "components": { + "type": "array", + "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." + }, + "modelCard": { + "$ref": "#/definitions/modelCard", + "title": "Machine Learning Model Card" + }, + "data": { + "type": "array", + "items": {"$ref": "#/definitions/componentData"}, + "title": "Data", + "description": "This object SHOULD be specified for any component of type `data` and MUST NOT be specified for other component types." + }, + "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.", + "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)." } - }, - "tool": { - "type": "object", - "title": "Tool", - "description": "Information about the automated or manual tool used", - "additionalProperties": false, + } + }, + "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": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the license elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + }, + "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" + }, + "licensing": { + "type": "object", + "title": "Licensing information", + "description": "Licensing details describing the licensor/licensee, license type, renewal and expiration dates, and other important metadata", + "additionalProperties": false, + "properties": { + "altIds": { + "type": "array", + "title": "Alternate License Identifiers", + "description": "License identifiers that may be used to manage licenses and their lifecycle", + "items": { + "type": "string" + } + }, + "licensor": { + "title": "Licensor", + "description": "The individual or organization that grants a license to another individual or organization", + "type": "object", + "additionalProperties": false, + "properties": { + "organization": { + "title": "Licensor (Organization)", + "description": "The organization that granted the license", + "$ref": "#/definitions/organizationalEntity" + }, + "individual": { + "title": "Licensor (Individual)", + "description": "The individual, not associated with an organization, that granted the license", + "$ref": "#/definitions/organizationalContact" + } + }, + "oneOf":[ + { + "required": ["organization"] + }, + { + "required": ["individual"] + } + ] + }, + "licensee": { + "title": "Licensee", + "description": "The individual or organization for which a license was granted to", + "type": "object", + "additionalProperties": false, + "properties": { + "organization": { + "title": "Licensee (Organization)", + "description": "The organization that was granted the license", + "$ref": "#/definitions/organizationalEntity" + }, + "individual": { + "title": "Licensee (Individual)", + "description": "The individual, not associated with an organization, that was granted the license", + "$ref": "#/definitions/organizationalContact" + } + }, + "oneOf":[ + { + "required": ["organization"] + }, + { + "required": ["individual"] + } + ] + }, + "purchaser": { + "title": "Purchaser", + "description": "The individual or organization that purchased the license", + "type": "object", + "additionalProperties": false, + "properties": { + "organization": { + "title": "Purchaser (Organization)", + "description": "The organization that purchased the license", + "$ref": "#/definitions/organizationalEntity" + }, + "individual": { + "title": "Purchaser (Individual)", + "description": "The individual, not associated with an organization, that purchased the license", + "$ref": "#/definitions/organizationalContact" + } + }, + "oneOf":[ + { + "required": ["organization"] + }, + { + "required": ["individual"] + } + ] + }, + "purchaseOrder": { + "type": "string", + "title": "Purchase Order", + "description": "The purchase order identifier the purchaser sent to a supplier or vendor to authorize a purchase" + }, + "licenseTypes": { + "type": "array", + "title": "License Type", + "description": "The type of license(s) that was granted to the licensee\n\n* __academic__ = A license that grants use of software solely for the purpose of education or research.\n* __appliance__ = A license covering use of software embedded in a specific piece of hardware.\n* __client-access__ = A Client Access License (CAL) allows client computers to access services provided by server software.\n* __concurrent-user__ = A Concurrent User license (aka floating license) limits the number of licenses for a software application and licenses are shared among a larger number of users.\n* __core-points__ = A license where the core of a computer's processor is assigned a specific number of points.\n* __custom-metric__ = A license for which consumption is measured by non-standard metrics.\n* __device__ = A license that covers a defined number of installations on computers and other types of devices.\n* __evaluation__ = A license that grants permission to install and use software for trial purposes.\n* __named-user__ = A license that grants access to the software to one or more pre-defined users.\n* __node-locked__ = A license that grants access to the software on one or more pre-defined computers or devices.\n* __oem__ = An Original Equipment Manufacturer license that is delivered with hardware, cannot be transferred to other hardware, and is valid for the life of the hardware.\n* __perpetual__ = A license where the software is sold on a one-time basis and the licensee can use a copy of the software indefinitely.\n* __processor-points__ = A license where each installation consumes points per processor.\n* __subscription__ = A license where the licensee pays a fee to use the software or service.\n* __user__ = A license that grants access to the software or service by a specified number of users.\n* __other__ = Another license type.\n", + "items": { + "type": "string", + "enum": [ + "academic", + "appliance", + "client-access", + "concurrent-user", + "core-points", + "custom-metric", + "device", + "evaluation", + "named-user", + "node-locked", + "oem", + "perpetual", + "processor-points", + "subscription", + "user", + "other" + ] + } + }, + "lastRenewal": { + "type": "string", + "format": "date-time", + "title": "Last Renewal", + "description": "The timestamp indicating when the license was last renewed. For new purchases, this is often the purchase or acquisition date. For non-perpetual licenses or subscriptions, this is the timestamp of when the license was last renewed." + }, + "expiration": { + "type": "string", + "format": "date-time", + "title": "Expiration", + "description": "The timestamp indicating when the current license expires (if applicable)." + } + } + }, "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." + "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.", + "items": {"$ref": "#/definitions/property"} + } + } + }, + "licenseChoice": { + "title": "License Choice", + "description": "EITHER (list of SPDX licenses and/or named licenses) OR (tuple of one SPDX License Expression)", + "type": "array", + "oneOf": [ + { + "title": "Multiple licenses", + "description": "A list of SPDX licenses and/or named licenses.", + "type": "array", + "items": { + "type": "object", + "required": ["license"], + "additionalProperties": false, + "properties": { + "license": {"$ref": "#/definitions/license"} + } } + }, + { + "title": "SPDX License Expression", + "description": "A tuple of exactly one SPDX License Expression.", + "type": "array", + "additionalItems": false, + "minItems": 1, + "maxItems": 1, + "items": [{ + "type": "object", + "additionalProperties": false, + "required": ["expression"], + "properties": { + "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" + ] + }, + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the license elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + } + } + }] } - }, - "organizationalEntity": { - "type": "object", - "title": "Organizational Entity Object", - "description": "", - "additionalProperties": false, - "properties": { - "name": { + ] + }, + "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", + "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", - "title": "Name", - "description": "The name of the organization", - "examples": [ - "Example Inc." - ] + "format": "iri-reference" }, - "url": { - "type": "array", - "items": { + "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": "External references provide a way to document systems, sites, and information that may be relevant, but are not included with the BOM. They may also establish specific relationships within or external to the BOM.", + "required": [ + "url", + "type" + ], + "additionalProperties": false, + "properties": { + "url": { + "anyOf": [ + { + "title": "URL", "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"} - } + { + "title": "BOM-Link", + "$ref": "#/definitions/bomLink" + } + ], + "title": "URL", + "description": "The URI (URL or URN) to the external reference. External references are URIs and therefore can accept any URL scheme including https ([RFC-7230](https://www.ietf.org/rfc/rfc7230.txt)), mailto ([RFC-2368](https://www.ietf.org/rfc/rfc2368.txt)), tel ([RFC-3966](https://www.ietf.org/rfc/rfc3966.txt)), and dns ([RFC-4501](https://www.ietf.org/rfc/rfc4501.txt)). External references may also include formally registered URNs such as [CycloneDX BOM-Link](https://cyclonedx.org/capabilities/bomlink/) to reference CycloneDX BOMs or any object within a BOM. BOM-Link transforms applicable external references into relationships that can be expressed in a BOM or across BOMs." + }, + "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.\n\n* __vcs__ = Version Control System\n* __issue-tracker__ = Issue or defect tracking system, or an Application Lifecycle Management (ALM) system\n* __website__ = Website\n* __advisories__ = Security advisories\n* __bom__ = Bill of Materials (SBOM, OBOM, HBOM, SaaSBOM, etc)\n* __mailing-list__ = Mailing list or discussion group\n* __social__ = Social media account\n* __chat__ = Real-time chat platform\n* __documentation__ = Documentation, guides, or how-to instructions\n* __support__ = Community or commercial support\n* __distribution__ = Direct or repository download location\n* __distribution-intake__ = The location where a component was published to. This is often the same as \"distribution\" but may also include specialized publishing processes that act as an intermediary\n* __license__ = 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\n* __build-meta__ = Build-system specific meta file (i.e. pom.xml, package.json, .nuspec, etc)\n* __build-system__ = URL to an automated build system\n* __release-notes__ = URL to release notes\n* __security-contact__ = Specifies a way to contact the maintainer, supplier, or provider in the event of a security incident. Common URIs include links to a disclosure procedure, a mailto (RFC-2368) that specifies an email address, a tel (RFC-3966) that specifies a phone number, or dns (RFC-4501) that specifies the records containing DNS Security TXT\n* __model-card__ = A model card describes the intended uses of a machine learning model, potential limitations, biases, ethical considerations, training parameters, datasets used to train the model, performance metrics, and other relevant data useful for ML transparency\n* __log__ = A record of events that occurred in a computer system or application, such as problems, errors, or information on current operations\n* __configuration__ = Parameters or settings that may be used by other components or services\n* __evidence__ = Information used to substantiate a claim\n* __formulation__ = Describes how a component or service was manufactured or deployed\n* __attestation__ = Human or machine-readable statements containing facts, evidence, or testimony\n* __threat-model__ = An enumeration of identified weaknesses, threats, and countermeasures, dataflow diagram (DFD), attack tree, and other supporting documentation in human-readable or machine-readable format\n* __adversary-model__ = The defined assumptions, goals, and capabilities of an adversary.\n* __risk-assessment__ = Identifies and analyzes the potential of future events that may negatively impact individuals, assets, and/or the environment. Risk assessments may also include judgments on the tolerability of each risk.\n* __vulnerability-assertion__ = A Vulnerability Disclosure Report (VDR) which asserts the known and previously unknown vulnerabilities that affect a component, service, or product including the analysis and findings describing the impact (or lack of impact) that the reported vulnerability has on a component, service, or product.\n* __exploitability-statement__ = A Vulnerability Exploitability eXchange (VEX) which asserts the known vulnerabilities that do not affect a product, product family, or organization, and optionally the ones that do. The VEX should include the analysis and findings describing the impact (or lack of impact) that the reported vulnerability has on the product, product family, or organization.\n* __pentest-report__ = Results from an authorized simulated cyberattack on a component or service, otherwise known as a penetration test\n* __static-analysis-report__ = SARIF or proprietary machine or human-readable report for which static analysis has identified code quality, security, and other potential issues with the source code\n* __dynamic-analysis-report__ = Dynamic analysis report that has identified issues such as vulnerabilities and misconfigurations\n* __runtime-analysis-report__ = Report generated by analyzing the call stack of a running application\n* __component-analysis-report__ = Report generated by Software Composition Analysis (SCA), container analysis, or other forms of component analysis\n* __maturity-report__ = Report containing a formal assessment of an organization, business unit, or team against a maturity model\n* __certification-report__ = Industry, regulatory, or other certification from an accredited (if applicable) certification body\n* __quality-metrics__ = Report or system in which quality metrics can be obtained\n* __codified-infrastructure__ = Code or configuration that defines and provisions virtualized infrastructure, commonly referred to as Infrastructure as Code (IaC)\n* __poam__ = Plans of Action and Milestones (POAM) compliment an \"attestation\" external reference. POAM is defined by NIST as a \"document that identifies tasks needing to be accomplished. It details resources required to accomplish the elements of the plan, any milestones in meeting the tasks and scheduled completion dates for the milestones\".\n* __other__ = Use this if no other types accurately describe the purpose of the external reference", + "enum": [ + "vcs", + "issue-tracker", + "website", + "advisories", + "bom", + "mailing-list", + "social", + "chat", + "documentation", + "support", + "distribution", + "distribution-intake", + "license", + "build-meta", + "build-system", + "release-notes", + "security-contact", + "model-card", + "log", + "configuration", + "evidence", + "formulation", + "attestation", + "threat-model", + "adversary-model", + "risk-assessment", + "vulnerability-assertion", + "exploitability-statement", + "pentest-report", + "static-analysis-report", + "dynamic-analysis-report", + "runtime-analysis-report", + "component-analysis-report", + "maturity-report", + "certification-report", + "codified-infrastructure", + "quality-metrics", + "poam", + "other" + ] + }, + "hashes": { + "type": "array", + "items": {"$ref": "#/definitions/hash"}, + "title": "Hashes", + "description": "The hashes of the external reference (if applicable)." } - }, - "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"] - } + } + }, + "dependency": { + "type": "object", + "title": "Dependency", + "description": "Defines the direct dependencies of a component or service. Components or services that do not have their own dependencies MUST be declared as empty elements within the graph. Components or services 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 object being dependency-free. It is RECOMMENDED to leverage compositions to indicate unknown dependency graphs.", + "required": [ + "ref" + ], + "additionalProperties": false, + "properties": { + "ref": { + "$ref": "#/definitions/refLinkType", + "title": "Reference", + "description": "References a component or service by its bom-ref attribute" + }, + "dependsOn": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/refLinkType" + }, + "title": "Depends On", + "description": "The bom-ref identifiers of the components or services that are dependencies of this dependency object." } - }, - "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": { + } + }, + "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", - "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"] + "format": "iri-reference" }, - "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)." + "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." + }, + "trustZone": { + "type": "string", + "title": "Trust Zone", + "description": "The name of the trust zone the service resides in." + }, + "data": { + "type": "array", + "items": {"$ref": "#/definitions/serviceData"}, + "title": "Data", + "description": "Specifies information about the data including the directional flow of data and the data classification." + }, + "licenses": { + "$ref": "#/definitions/licenseChoice", + "title": "Component License(s)" + }, + "externalReferences": { + "type": "array", + "items": {"$ref": "#/definitions/externalReference"}, + "title": "External References", + "description": "External references provide a way to document systems, sites, and information that may be relevant, but are not included with the BOM. They may also establish specific relationships within or external to the BOM." + }, + "services": { + "type": "array", + "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.", + "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)." + } + } + }, + "serviceData": { + "type": "object", + "title": "Hash Objects", + "required": [ + "flow", + "classification" + ], + "additionalProperties": false, + "properties": { + "flow": { + "$ref": "#/definitions/dataFlowDirection", + "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": { + "$ref": "#/definitions/dataClassification" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name for the defined data", + "examples": [ + "Credit card reporting" + ] + }, + "description": { + "type": "string", + "title": "Description", + "description": "Short description of the data content and usage", + "examples": [ + "Credit card information being exchanged in between the web app and the database" + ] + }, + "governance": { + "type": "object", + "title": "Data Governance", + "$ref": "#/definitions/dataGovernance" + }, + "source": { + "type": "array", + "items": { + "anyOf": [ + { + "title": "URL", + "type": "string", + "format": "iri-reference" + }, + { + "title": "BOM-Link Element", + "$ref": "#/definitions/bomLinkElementType" + } + ] }, - "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." + "title": "Source", + "description": "The URI, URL, or BOM-Link of the components or services the data came in from" + }, + "destination": { + "type": "array", + "items": { + "anyOf": [ + { + "title": "URL", + "type": "string", + "format": "iri-reference" + }, + { + "title": "BOM-Link Element", + "$ref": "#/definitions/bomLinkElementType" + } + ] }, - "pedigree": { + "title": "Destination", + "description": "The URI, URL, or BOM-Link of the components or services the data is sent to" + } + } + }, + "dataFlowDirection": { + "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": { + "identity": { + "type": "object", + "description": "Evidence that substantiates the identity of a component.", + "required": [ "field" ], + "additionalProperties": false, + "properties": { + "field": { + "type": "string", + "enum": [ + "group", "name", "version", "purl", "cpe", "swid", "hash" + ], + "title": "Field", + "description": "The identity field of the component which the evidence describes." + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "title": "Confidence", + "description": "The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence." + }, + "methods": { + "type": "array", + "title": "Methods", + "description": "The methods used to extract and/or analyze the evidence.", + "items": { + "type": "object", + "required": [ + "technique" , + "confidence" + ], + "additionalProperties": false, + "properties": { + "technique": { + "title": "Technique", + "description": "The technique used in this method of analysis.", + "type": "string", + "enum": [ + "source-code-analysis", + "binary-analysis", + "manifest-analysis", + "ast-fingerprint", + "hash-comparison", + "instrumentation", + "dynamic-analysis", + "filename", + "attestation", + "other" + ] + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "title": "Confidence", + "description": "The confidence of the evidence from 0 - 1, where 1 is 100% confidence. Confidence is specific to the technique used. Each technique of analysis can have independent confidence." + }, + "value": { + "type": "string", + "title": "Value", + "description": "The value or contents of the evidence." + } + } + } + }, + "tools": { + "type": "array", + "uniqueItems": true, + "items": { + "anyOf": [ + { + "title": "Ref", + "$ref": "#/definitions/refLinkType" + }, + { + "title": "BOM-Link Element", + "$ref": "#/definitions/bomLinkElementType" + } + ] + }, + "title": "BOM References", + "description": "The object in the BOM identified by its bom-ref. This is often a component or service, but may be any object type supporting bom-refs. Tools used for analysis should already be defined in the BOM, either in the metadata/tools, components, or formulation." + } + } + }, + "occurrences": { + "type": "array", + "title": "Occurrences", + "description": "Evidence of individual instances of a component spread across multiple locations.", + "items": { "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.", + "required": [ "location" ], "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"} + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the occurrence elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." }, - "notes": { + "location": { "type": "string", - "title": "Notes", - "description": "Notes, observations, and other non-structured commentary describing the components pedigree." + "title": "Location", + "description": "The location or path to where the component was found." } } - }, - "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." - }, + } + }, + "callstack": { + "type": "object", + "description": "Evidence of the components use through the callstack.", + "additionalProperties": false, "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)." + "frames": { + "type": "array", + "title": "Methods", + "items": { + "type": "object", + "required": [ + "module" + ], + "additionalProperties": false, + "properties": { + "package": { + "title": "Package", + "description": "A package organizes modules into namespaces, providing a unique namespace for each type it contains.", + "type": "string" + }, + "module": { + "title": "Module", + "description": "A module or class that encloses functions/methods and other code.", + "type": "string" + }, + "function": { + "title": "Function", + "description": "A block of code designed to perform a particular task.", + "type": "string" + }, + "parameters": { + "title": "Parameters", + "description": "Optional arguments that are passed to the module or function.", + "type": "array", + "items": { + "type": "string" + } + }, + "line": { + "title": "Line", + "description": "The line number the code that is called resides on.", + "type": "integer" + }, + "column": { + "title": "Column", + "description": "The column the code that is called resides.", + "type": "integer" + }, + "fullFilename": { + "title": "Full Filename", + "description": "The full path and filename of the module.", + "type": "string" + } + } + } + } } + }, + "licenses": { + "$ref": "#/definitions/licenseChoice", + "title": "Component License(s)" + }, + "copyright": { + "type": "array", + "items": {"$ref": "#/definitions/copyright"}, + "title": "Copyright" } - }, - "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" + } + }, + "compositions": { + "type": "object", + "title": "Compositions", + "required": [ + "aggregate" + ], + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the composition elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + }, + "aggregate": { + "$ref": "#/definitions/aggregateType", + "title": "Aggregate", + "description": "Specifies an aggregate type that describe how complete a relationship is.\n\n* __complete__ = The relationship is complete. No further relationships including constituent components, services, or dependencies are known to exist.\n* __incomplete__ = The relationship is incomplete. Additional relationships exist and may include constituent components, services, or dependencies.\n* __incomplete_first_party_only__ = The relationship is incomplete. Only relationships for first-party components, services, or their dependencies are represented.\n* __incomplete_first_party_proprietary_only__ = The relationship is incomplete. Only relationships for first-party components, services, or their dependencies are represented, limited specifically to those that are proprietary.\n* __incomplete_first_party_opensource_only__ = The relationship is incomplete. Only relationships for first-party components, services, or their dependencies are represented, limited specifically to those that are opensource.\n* __incomplete_third_party_only__ = The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented.\n* __incomplete_third_party_proprietary_only__ = The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented, limited specifically to those that are proprietary.\n* __incomplete_third_party_opensource_only__ = The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented, limited specifically to those that are opensource.\n* __unknown__ = 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.\n* __not_specified__ = The relationship completeness is not specified.\n" + }, + "assemblies": { + "type": "array", + "uniqueItems": true, + "items": { + "anyOf": [ + { + "title": "Ref", + "$ref": "#/definitions/refLinkType" + }, + { + "title": "BOM-Link Element", + "$ref": "#/definitions/bomLinkElementType" + } + ] }, - "url": { - "type": "string", - "title": "URL", - "description": "The URL to the SWID file.", - "format": "iri-reference" - } + "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." + }, + "vulnerabilities": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + }, + "title": "BOM references", + "description": "The bom-ref identifiers of the vulnerabilities being described." + }, + "signature": { + "$ref": "#/definitions/signature", + "title": "Signature", + "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)." } - }, - "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" + } + }, + "aggregateType": { + "type": "string", + "default": "not_specified", + "enum": [ + "complete", + "incomplete", + "incomplete_first_party_only", + "incomplete_first_party_proprietary_only", + "incomplete_first_party_opensource_only", + "incomplete_third_party_only", + "incomplete_third_party_proprietary_only", + "incomplete_third_party_opensource_only", + "unknown", + "not_specified" + ] + }, + "property": { + "type": "object", + "title": "Lightweight name-value pair", + "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.", + "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" }, - "encoding": { - "type": "string", - "title": "Encoding", - "description": "Specifies the optional encoding the text is represented in.", - "enum": [ - "base64" - ] + "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" }, - "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, + "title": "Tags", + "description": "One or more tags that may aid in search or retrieval of the release note." + }, + "resolves": { + "type": "array", + "items": {"$ref": "#/definitions/issue"}, + "title": "Resolves", + "description": "A collection of issues that have been resolved." + }, + "notes": { + "type": "array", + "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": { - "alg": { - "$ref": "#/definitions/hash-alg" - }, - "content": { - "$ref": "#/definitions/hash-content" - } + "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.", + "items": {"$ref": "#/definitions/property"} } - }, - "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"] + } + }, + "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* CVSSv4 - [Common Vulnerability Scoring System v4](https://www.first.org/cvss/v4-0/)\n* OWASP - [OWASP Risk Rating Methodology](https://owasp.org/www-community/OWASP_Risk_Rating_Methodology)\n* SSVC - [Stakeholder Specific Vulnerability Categorization](https://github.com/CERTCC/SSVC) (all versions)", + "enum": [ + "CVSSv2", + "CVSSv3", + "CVSSv31", + "CVSSv4", + "OWASP", + "SSVC", + "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 a 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.", + "items": { + "type": "object", + "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." + } + } } - ], - "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" + }, + "ratings": { + "type": "array", + "title": "Ratings", + "description": "List of vulnerability ratings", + "items": { + "$ref": "#/definitions/rating" } - } - }, - "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" - ] + }, + "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], + "items": { + "$ref": "#/definitions/cwe" } }, - "oneOf":[ - { - "required": ["license"] - }, - { - "required": ["expression"] + "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 information useful in understanding root cause." + }, + "recommendation": { + "type": "string", + "title": "Recommendation", + "description": "Recommendations of how the vulnerability can be remediated or mitigated." + }, + "workaround": { + "type": "string", + "title": "Workarounds", + "description": "A bypass, usually temporary, of the vulnerability that reduces its likelihood and/or impact. Workarounds often involve changes to configuration or deployments." + }, + "proofOfConcept": { + "type": "object", + "title": "Proof of Concept", + "description": "Evidence used to reproduce the vulnerability.", + "properties": { + "reproductionSteps": { + "type": "string", + "title": "Steps to Reproduce", + "description": "Precise steps to reproduce the vulnerability." + }, + "environment": { + "type": "string", + "title": "Environment", + "description": "A description of the environment in which reproduction was possible." + }, + "supportingMaterial": { + "type": "array", + "title": "Supporting Material", + "description": "Supporting material that helps in reproducing or understanding how reproduction is possible. This may include screenshots, payloads, and PoC exploit code.", + "items": { "$ref": "#/definitions/attachment" } + } } - ] - }, - "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" + }, + "advisories": { + "type": "array", + "title": "Advisories", + "description": "Published advisories of the vulnerability if provided.", + "items": { + "$ref": "#/definitions/advisory" } - } - }, - "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" + }, + "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." + }, + "rejected": { + "type": "string", + "format": "date-time", + "title": "Rejected", + "description": "The date and time (timestamp) when the vulnerability record was rejected (if applicable)." + }, + "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.", + "items": { + "$ref": "#/definitions/organizationalEntity" + } + }, + "individuals": { + "type": "array", + "title": "Individuals", + "description": "The individuals, not associated with organizations, that are credited with vulnerability discovery.", + "items": { + "$ref": "#/definitions/organizationalContact" + } + } } - } - }, - "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" + }, + "tools": { + "oneOf": [ + { + "type": "object", + "title": "Tools", + "description": "The tool(s) used to identify, confirm, or score the vulnerability.", + "additionalProperties": false, + "properties": { + "components": { + "type": "array", + "items": {"$ref": "#/definitions/component"}, + "uniqueItems": true, + "title": "Components", + "description": "A list of software and hardware components used as tools" + }, + "services": { + "type": "array", + "items": {"$ref": "#/definitions/service"}, + "uniqueItems": true, + "title": "Services", + "description": "A list of services used as tools. This may include microservices, function-as-a-service, and other types of network or intra-process services." + } + } + }, + { + "type": "array", + "title": "Tools (legacy)", + "description": "[Deprecated] The tool(s) used to identify, confirm, or score the vulnerability.", + "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.", + "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." + }, + "firstIssued": { + "type": "string", + "format": "date-time", + "title": "First Issued", + "description": "The date and time (timestamp) when the analysis was first issued." + }, + "lastUpdated": { + "type": "string", + "format": "date-time", + "title": "Last Updated", + "description": "The date and time (timestamp) when the analysis was last updated." + } } - } - }, - "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": { + }, + "affects": { + "type": "array", + "uniqueItems": true, + "items": { "type": "object", - "title": "Source", - "description": "The source of the issue where it is documented", + "required": [ + "ref" + ], "additionalProperties": false, "properties": { - "name": { - "type": "string", - "title": "Name", - "description": "The name of the source. For example 'National Vulnerability Database', 'NVD', and 'Apache'" + "ref": { + "anyOf": [ + { + "title": "Ref", + "$ref": "#/definitions/refLinkType" + }, + { + "title": "BOM-Link Element", + "$ref": "#/definitions/bomLinkElementType" + } + ], + "title": "Reference", + "description": "References a component or service by the objects bom-ref" }, - "url": { - "type": "string", - "title": "URL", - "description": "The url of the issue documentation as provided by the source", - "format": "iri-reference" + "versions": { + "type": "array", + "title": "Versions", + "description": "Zero or more individual versions or range of versions.", + "items": { + "type": "object", + "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/range" + }, + "status": { + "description": "The vulnerability status for the version or range of versions.", + "$ref": "#/definitions/affectedStatus", + "default": "affected" + } + } + } } } }, - "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, + "title": "Affects", + "description": "The components or services that are affected by the vulnerability." + }, "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" + "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.", + "items": { + "$ref": "#/definitions/property" } } - }, - "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" + } + }, + "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 + }, + "annotations": { + "type": "object", + "title": "Annotations", + "description": "A comment, note, explanation, or similar textual content which provides additional context to the object(s) being annotated.", + "required": [ + "subjects", + "annotator", + "timestamp", + "text" + ], + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the annotation elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + }, + "subjects": { + "type": "array", + "uniqueItems": true, + "items": { + "anyOf": [ + { + "title": "Ref", + "$ref": "#/definitions/refLinkType" + }, + { + "title": "BOM-Link Element", + "$ref": "#/definitions/bomLinkElementType" + } ] }, - "hashes": { - "type": "array", - "additionalItems": false, - "items": {"$ref": "#/definitions/hash"}, - "title": "Hashes", - "description": "The hashes of the external reference (if applicable)." + "title": "BOM References", + "description": "The object in the BOM identified by its bom-ref. This is often a component or service, but may be any object type supporting bom-refs." + }, + "annotator": { + "type": "object", + "title": "Annotator", + "description": "The organization, person, component, or service which created the textual content of the annotation.", + "oneOf": [ + { + "required": [ + "organization" + ] + }, + { + "required": [ + "individual" + ] + }, + { + "required": [ + "component" + ] + }, + { + "required": [ + "service" + ] + } + ], + "additionalProperties": false, + "properties": { + "organization": { + "description": "The organization that created the annotation", + "$ref": "#/definitions/organizationalEntity" + }, + "individual": { + "description": "The person that created the annotation", + "$ref": "#/definitions/organizationalContact" + }, + "component": { + "description": "The tool or component that created the annotation", + "$ref": "#/definitions/component" + }, + "service": { + "description": "The service that created the annotation", + "$ref": "#/definitions/service" + } } + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp", + "description": "The date and time (timestamp) when the annotation was created." + }, + "text": { + "type": "string", + "title": "Text", + "description": "The textual content of the annotation." + }, + "signature": { + "$ref": "#/definitions/signature", + "title": "Signature", + "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)." } - }, - "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" + } + }, + "modelCard": { + "$comment": "Model card support in CycloneDX is derived from TensorFlow Model Card Toolkit released under the Apache 2.0 license and available from https://github.com/tensorflow/model-card-toolkit/blob/main/model_card_toolkit/schema/v0.0.2/model_card.schema.json. In addition, CycloneDX model card support includes portions of VerifyML, also released under the Apache 2.0 license and available from https://github.com/cylynx/verifyml/blob/main/verifyml/model_card_toolkit/schema/v0.0.4/model_card.schema.json.", + "type": "object", + "title": "Model Card", + "description": "A model card describes the intended uses of a machine learning model and potential limitations, including biases and ethical considerations. Model cards typically contain the training parameters, which datasets were used to train the model, performance metrics, and other relevant data useful for ML transparency. This object SHOULD be specified for any component of type `machine-learning-model` and MUST NOT be specified for other component types.", + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the model card elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + }, + "modelParameters": { + "type": "object", + "title": "Model Parameters", + "description": "Hyper-parameters for construction of the model.", + "additionalProperties": false, + "properties": { + "approach": { + "type": "object", + "title": "Approach", + "description": "The overall approach to learning used by the model for problem solving.", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "title": "Learning Type", + "description": "Learning types describing the learning problem or hybrid learning problem.", + "enum": [ + "supervised", + "unsupervised", + "reinforcement-learning", + "semi-supervised", + "self-supervised" + ] + } + } + }, + "task": { + "type": "string", + "title": "Task", + "description": "Directly influences the input and/or output. Examples include classification, regression, clustering, etc." + }, + "architectureFamily": { + "type": "string", + "title": "Architecture Family", + "description": "The model architecture family such as transformer network, convolutional neural network, residual neural network, LSTM neural network, etc." + }, + "modelArchitecture": { + "type": "string", + "title": "Model Architecture", + "description": "The specific architecture of the model such as GPT-1, ResNet-50, YOLOv3, etc." + }, + "datasets": { + "type": "array", + "title": "Datasets", + "description": "The datasets used to train and evaluate the model.", + "items" : { + "oneOf" : [ + { + "title": "Inline Component Data", + "$ref": "#/definitions/componentData" + }, + { + "type": "object", + "title": "Data Component Reference", + "additionalProperties": false, + "properties": { + "ref": { + "anyOf": [ + { + "title": "Ref", + "$ref": "#/definitions/refLinkType" + }, + { + "title": "BOM-Link Element", + "$ref": "#/definitions/bomLinkElementType" + } + ], + "title": "Reference", + "description": "References a data component by the components bom-ref attribute" + } + } + } + ] + } + }, + "inputs": { + "type": "array", + "title": "Inputs", + "description": "The input format(s) of the model", + "items": { "$ref": "#/definitions/inputOutputMLParameters" } }, - "title": "Depends On", - "description": "The bom-ref identifiers of the components that are dependencies of this dependency object." + "outputs": { + "type": "array", + "title": "Outputs", + "description": "The output format(s) from the model", + "items": { "$ref": "#/definitions/inputOutputMLParameters" } + } } - } - }, - "service": { - "type": "object", - "title": "Service Object", - "required": [ - "name" - ], - "additionalProperties": false, + }, + "quantitativeAnalysis": { + "type": "object", + "title": "Quantitative Analysis", + "description": "A quantitative analysis of the model", + "additionalProperties": false, + "properties": { + "performanceMetrics": { + "type": "array", + "title": "Performance Metrics", + "description": "The model performance metrics being reported. Examples may include accuracy, F1 score, precision, top-3 error rates, MSC, etc.", + "items": { "$ref": "#/definitions/performanceMetric" } + }, + "graphics": { "$ref": "#/definitions/graphicsCollection" } + } + }, + "considerations": { + "type": "object", + "title": "Considerations", + "description": "What considerations should be taken into account regarding the model's construction, training, and application?", + "additionalProperties": false, + "properties": { + "users": { + "type": "array", + "title": "Users", + "description": "Who are the intended users of the model?", + "items": { + "type": "string" + } + }, + "useCases": { + "type": "array", + "title": "Use Cases", + "description": "What are the intended use cases of the model?", + "items": { + "type": "string" + } + }, + "technicalLimitations": { + "type": "array", + "title": "Technical Limitations", + "description": "What are the known technical limitations of the model? E.g. What kind(s) of data should the model be expected not to perform well on? What are the factors that might degrade model performance?", + "items": { + "type": "string" + } + }, + "performanceTradeoffs": { + "type": "array", + "title": "Performance Tradeoffs", + "description": "What are the known tradeoffs in accuracy/performance of the model?", + "items": { + "type": "string" + } + }, + "ethicalConsiderations": { + "type": "array", + "title": "Ethical Considerations", + "description": "What are the ethical (or environmental) risks involved in the application of this model?", + "items": { "$ref": "#/definitions/risk" } + }, + "fairnessAssessments": { + "type": "array", + "title": "Fairness Assessments", + "description": "How does the model affect groups at risk of being systematically disadvantaged? What are the harms and benefits to the various affected groups?", + "items": { + "$ref": "#/definitions/fairnessAssessment" + } + } + } + }, "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": "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.", + "items": {"$ref": "#/definitions/property"} + } + } + }, + "inputOutputMLParameters": { + "type": "object", + "title": "Input and Output Parameters", + "additionalProperties": false, + "properties": { + "format": { + "description": "The data format for input/output to the model. Example formats include string, image, time-series", + "type": "string" + } + } + }, + "componentData": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the dataset elsewhere in the BOM. Every bom-ref MUST be unique within the BOM." + }, + "type": { + "type": "string", + "title": "Type of Data", + "description": "The general theme or subject matter of the data being specified.\n\n* __source-code__ = Any type of code, code snippet, or data-as-code.\n* __configuration__ = Parameters or settings that may be used by other components.\n* __dataset__ = A collection of data.\n* __definition__ = Data that can be used to create new instances of what the definition defines.\n* __other__ = Any other type of data that does not fit into existing definitions.", + "enum": [ + "source-code", + "configuration", + "dataset", + "definition", + "other" + ] + }, + "name": { + "description": "The name of the dataset.", + "type": "string" + }, + "contents": { + "type": "object", + "title": "Data Contents", + "description": "The contents or references to the contents of the data being described.", + "additionalProperties": false, + "properties": { + "attachment": { + "title": "Data Attachment", + "description": "An optional way to include textual or encoded data.", + "$ref": "#/definitions/attachment" + }, + "url": { "type": "string", + "title": "Data URL", + "description": "The URL to where the data can be retrieved.", "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": "Configuration Properties", + "description": "Provides the ability to document name-value parameters used for configuration.", + "items": { + "$ref": "#/definitions/property" + } + } + } + }, + "classification": { + "$ref": "#/definitions/dataClassification" + }, + "sensitiveData": { + "type": "array", + "description": "A description of any sensitive data in a dataset.", + "items": { + "type": "string" + } + }, + "graphics": { "$ref": "#/definitions/graphicsCollection" }, + "description": { + "description": "A description of the dataset. Can describe size of dataset, whether it's used for source code, training, testing, or validation, etc.", + "type": "string" + }, + "governance": { + "type": "object", + "title": "Data Governance", + "$ref": "#/definitions/dataGovernance" + } + } + }, + "dataGovernance": { + "type": "object", + "title": "Data Governance", + "additionalProperties": false, + "properties": { + "custodians": { + "type": "array", + "title": "Data Custodians", + "description": "Data custodians are responsible for the safe custody, transport, and storage of data.", + "items": { "$ref": "#/definitions/dataGovernanceResponsibleParty" } + }, + "stewards": { + "type": "array", + "title": "Data Stewards", + "description": "Data stewards are responsible for data content, context, and associated business rules.", + "items": { "$ref": "#/definitions/dataGovernanceResponsibleParty" } + }, + "owners": { + "type": "array", + "title": "Data Owners", + "description": "Data owners are concerned with risk and appropriate access to data.", + "items": { "$ref": "#/definitions/dataGovernanceResponsibleParty" } + } + } + }, + "dataGovernanceResponsibleParty": { + "type": "object", + "additionalProperties": false, + "properties": { + "organization": { + "title": "Organization", + "$ref": "#/definitions/organizationalEntity" + }, + "contact": { + "title": "Individual", + "$ref": "#/definitions/organizationalContact" + } + }, + "oneOf":[ + { + "required": ["organization"] + }, + { + "required": ["contact"] + } + ] + }, + "graphicsCollection": { + "type": "object", + "title": "Graphics Collection", + "description": "A collection of graphics that represent various measurements.", + "additionalProperties": false, + "properties": { + "description": { + "description": "A description of this collection of graphics.", + "type": "string" + }, + "collection": { + "description": "A collection of graphics.", + "type": "array", + "items": { "$ref": "#/definitions/graphic" } + } + } + }, + "graphic": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "The name of the graphic.", + "type": "string" + }, + "image": { + "title": "Graphic Image", + "description": "The graphic (vector or raster). Base64 encoding MUST be specified for binary images.", + "$ref": "#/definitions/attachment" + } + } + }, + "performanceMetric": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "description": "The type of performance metric.", + "type": "string" + }, + "value": { + "description": "The value of the performance metric.", + "type": "string" + }, + "slice": { + "description": "The name of the slice this metric was computed on. By default, assume this metric is not sliced.", + "type": "string" + }, + "confidenceInterval": { + "description": "The confidence interval of the metric.", + "type": "object", + "additionalProperties": false, "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"} + "lowerBound": { + "description": "The lower bound of the confidence interval.", + "type": "string" + }, + "upperBound": { + "description": "The upper bound of the confidence interval.", + "type": "string" + } + } + } + } + }, + "risk": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "The name of the risk.", + "type": "string" + }, + "mitigationStrategy": { + "description": "Strategy used to address this risk.", + "type": "string" + } + } + }, + "fairnessAssessment": { + "type": "object", + "title": "Fairness Assessment", + "description": "Information about the benefits and harms of the model to an identified at risk group.", + "additionalProperties": false, + "properties": { + "groupAtRisk": { + "type": "string", + "description": "The groups or individuals at risk of being systematically disadvantaged by the model." + }, + "benefits": { + "type": "string", + "description": "Expected benefits to the identified groups." + }, + "harms": { + "type": "string", + "description": "Expected harms to the identified groups." + }, + "mitigationStrategy": { + "type": "string", + "description": "With respect to the benefits and harms outlined, please describe any mitigation strategy implemented." + } + } + }, + "dataClassification": { + "type": "string", + "title": "Data Classification", + "description": "Data classification tags data according to its type, sensitivity, and value if altered, stolen, or destroyed." + }, + "formula": { + "title": "Formula", + "description": "Describes workflows and resources that captures rules and other aspects of how the associated BOM component or service was formed.", + "type": "object", + "additionalProperties": false, + "properties": { + "bom-ref": { + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the formula elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.", + "$ref": "#/definitions/refType" + }, + "components": { + "title": "Components", + "description": "Transient components that are used in tasks that constitute one or more of this formula's workflows", + "type": "array", + "items": { + "$ref": "#/definitions/component" }, - "signature": { - "$ref": "#/definitions/signature", - "title": "Signature", - "description": "Enveloped signature in [JSON Signature Format (JSF)](https://cyberphone.github.io/doc/security/jsf.html)." + "uniqueItems": true + }, + "services": { + "title": "Services", + "description": "Transient services that are used in tasks that constitute one or more of this formula's workflows", + "type": "array", + "items": { + "$ref": "#/definitions/service" + }, + "uniqueItems": true + }, + "workflows": { + "title": "Workflows", + "description": "List of workflows that can be declared to accomplish specific orchestrated goals and independently triggered.", + "$comment": "Different workflows can be designed to work together to perform end-to-end CI/CD builds and deployments.", + "type": "array", + "items": { + "$ref": "#/definitions/workflow" + }, + "uniqueItems": true + }, + "properties": { + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" } } - }, - "dataClassification": { - "type": "object", - "title": "Hash Objects", - "required": [ - "flow", - "classification" - ], - "additionalProperties": false, + } + }, + "workflow": { + "title": "Workflow", + "description": "A specialized orchestration task.", + "$comment": "Workflow are as task themselves and can trigger other workflow tasks. These relationships can be modeled in the taskDependencies graph.", + "type": "object", + "required": [ + "bom-ref", + "uid", + "taskTypes" + ], + "additionalProperties": false, + "properties": { + "bom-ref": { + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the workflow elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.", + "$ref": "#/definitions/refType" + }, + "uid": { + "title": "Unique Identifier (UID)", + "description": "The unique identifier for the resource instance within its deployment context.", + "type": "string" + }, + "name": { + "title": "Name", + "description": "The name of the resource instance.", + "type": "string" + }, + "description": { + "title": "Description", + "description": "A description of the resource instance.", + "type": "string" + }, + "resourceReferences": { + "title": "Resource references", + "description": "References to component or service resources that are used to realize the resource instance.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/resourceReferenceChoice" + } + }, + "tasks": { + "title": "Tasks", + "description": "The tasks that comprise the workflow.", + "$comment": "Note that tasks can appear more than once as different instances (by name or UID).", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/task" + } + }, + "taskDependencies": { + "title": "Task dependency graph", + "description": "The graph of dependencies between tasks within the workflow.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/dependency" + } + }, + "taskTypes": { + "title": "Task types", + "description": "Indicates the types of activities performed by the set of workflow tasks.", + "$comment": "Currently, these types reflect common CI/CD actions.", + "type": "array", + "items": { + "$ref": "#/definitions/taskType" + } + }, + "trigger": { + "title": "Trigger", + "description": "The trigger that initiated the task.", + "$ref": "#/definitions/trigger" + }, + "steps": { + "title": "Steps", + "description": "The sequence of steps for the task.", + "type": "array", + "items": { + "$ref": "#/definitions/step" + }, + "uniqueItems": true + }, + "inputs": { + "title": "Inputs", + "description": "Represents resources and data brought into a task at runtime by executor or task commands", + "examples": ["a `configuration` file which was declared as a local `component` or `externalReference`"], + "type": "array", + "items": { + "$ref": "#/definitions/inputType" + }, + "uniqueItems": true + }, + "outputs": { + "title": "Outputs", + "description": "Represents resources and data output from a task at runtime by executor or task commands", + "examples": ["a log file or metrics data produced by the task"], + "type": "array", + "items": { + "$ref": "#/definitions/outputType" + }, + "uniqueItems": true + }, + "timeStart": { + "title": "Time start", + "description": "The date and time (timestamp) when the task started.", + "type": "string", + "format": "date-time" + }, + "timeEnd": { + "title": "Time end", + "description": "The date and time (timestamp) when the task ended.", + "type": "string", + "format": "date-time" + }, + "workspaces": { + "title": "Workspaces", + "description": "A set of named filesystem or data resource shareable by workflow tasks.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/workspace" + } + }, + "runtimeTopology": { + "title": "Runtime topology", + "description": "A graph of the component runtime topology for workflow's instance.", + "$comment": "A description of the runtime component and service topology. This can describe a partial or complete topology used to host and execute the task (e.g., hardware, operating systems, configurations, etc.),", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/dependency" + } + }, "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." + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" } } - }, - "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, + } + }, + "task": { + "title": "Task", + "description": "Describes the inputs, sequence of steps and resources used to accomplish a task and its output.", + "$comment": "Tasks are building blocks for constructing assemble CI/CD workflows or pipelines.", + "type": "object", + "required": [ + "bom-ref", + "uid", + "taskTypes" + ], + "additionalProperties": false, + "properties": { + "bom-ref": { + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the task elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.", + "$ref": "#/definitions/refType" + }, + "uid": { + "title": "Unique Identifier (UID)", + "description": "The unique identifier for the resource instance within its deployment context.", + "type": "string" + }, + "name": { + "title": "Name", + "description": "The name of the resource instance.", + "type": "string" + }, + "description": { + "title": "Description", + "description": "A description of the resource instance.", + "type": "string" + }, + "resourceReferences": { + "title": "Resource references", + "description": "References to component or service resources that are used to realize the resource instance.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/resourceReferenceChoice" + } + }, + "taskTypes": { + "title": "Task types", + "description": "Indicates the types of activities performed by the set of workflow tasks.", + "$comment": "Currently, these types reflect common CI/CD actions.", + "type": "array", + "items": { + "$ref": "#/definitions/taskType" + } + }, + "trigger": { + "title": "Trigger", + "description": "The trigger that initiated the task.", + "$ref": "#/definitions/trigger" + }, + "steps": { + "title": "Steps", + "description": "The sequence of steps for the task.", + "type": "array", + "items": { + "$ref": "#/definitions/step" + }, + "uniqueItems": true + }, + "inputs": { + "title": "Inputs", + "description": "Represents resources and data brought into a task at runtime by executor or task commands", + "examples": ["a `configuration` file which was declared as a local `component` or `externalReference`"], + "type": "array", + "items": { + "$ref": "#/definitions/inputType" + }, + "uniqueItems": true + }, + "outputs": { + "title": "Outputs", + "description": "Represents resources and data output from a task at runtime by executor or task commands", + "examples": ["a log file or metrics data produced by the task"], + "type": "array", + "items": { + "$ref": "#/definitions/outputType" + }, + "uniqueItems": true + }, + "timeStart": { + "title": "Time start", + "description": "The date and time (timestamp) when the task started.", + "type": "string", + "format": "date-time" + }, + "timeEnd": { + "title": "Time end", + "description": "The date and time (timestamp) when the task ended.", + "type": "string", + "format": "date-time" + }, + "workspaces": { + "title": "Workspaces", + "description": "A set of named filesystem or data resource shareable by workflow tasks.", + "type": "array", + "items": { + "$ref": "#/definitions/workspace" + }, + "uniqueItems": true + }, + "runtimeTopology": { + "title": "Runtime topology", + "description": "A graph of the component runtime topology for task's instance.", + "$comment": "A description of the runtime component and service topology. This can describe a partial or complete topology used to host and execute the task (e.g., hardware, operating systems, configurations, etc.),", + "type": "array", + "items": { + "$ref": "#/definitions/dependency" + }, + "uniqueItems": true + }, "properties": { - "text": { - "type": "string", - "title": "Copyright Text" + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" } } - }, - - "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" + } + }, + "step": { + "type": "object", + "description": "Executes specific commands or tools in order to accomplish its owning task as part of a sequence.", + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "description": "A name for the step.", + "type": "string" + }, + "description": { + "title": "Description", + "description": "A description of the step.", + "type": "string" + }, + "commands": { + "title": "Commands", + "description": "Ordered list of commands or directives for the step", + "type": "array", + "items": { + "$ref": "#/definitions/command" } - } - }, - "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)." + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" } } - }, - "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", + } + }, + "command": { + "type": "object", + "additionalProperties": false, + "properties": { + "executed": { + "title": "Executed", + "description": "A text representation of the executed command.", + "type": "string" + }, "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." + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/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" + } + }, + "workspace": { + "title": "Workspace", + "description": "A named filesystem or data resource shareable by workflow tasks.", + "type": "object", + "required": [ + "bom-ref", + "uid" + ], + "additionalProperties": false, + "properties": { + "bom-ref": { + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the workspace elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.", + "$ref": "#/definitions/refType" + }, + "uid": { + "title": "Unique Identifier (UID)", + "description": "The unique identifier for the resource instance within its deployment context.", + "type": "string" + }, + "name": { + "title": "Name", + "description": "The name of the resource instance.", + "type": "string" + }, + "aliases": { + "title": "Aliases", + "description": "The names for the workspace as referenced by other workflow tasks. Effectively, a name mapping so other tasks can use their own local name in their steps.", + "type": "array", + "items": {"type": "string"} + }, + "description": { + "title": "Description", + "description": "A description of the resource instance.", + "type": "string" + }, + "resourceReferences": { + "title": "Resource references", + "description": "References to component or service resources that are used to realize the resource instance.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/resourceReferenceChoice" } - } - }, - "releaseNotes": { - "type": "object", - "title": "Release notes", - "required": [ - "type" - ], - "additionalProperties": false, + }, + "accessMode": { + "title": "Access mode", + "description": "Describes the read-write access control for the workspace relative to the owning resource instance.", + "type": "string", + "enum": [ + "read-only", + "read-write", + "read-write-once", + "write-once", + "write-only" + ] + }, + "mountPath": { + "title": "Mount path", + "description": "A path to a location on disk where the workspace will be available to the associated task's steps.", + "type": "string" + }, + "managedDataType": { + "title": "Managed data type", + "description": "The name of a domain-specific data type the workspace represents.", + "$comment": "This property is for CI/CD frameworks that are able to provide access to structured, managed data at a more granular level than a filesystem.", + "examples": ["ConfigMap","Secret"], + "type": "string" + }, + "volumeRequest": { + "title": "Volume request", + "description": "Identifies the reference to the request for a specific volume type and parameters.", + "examples": ["a kubernetes Persistent Volume Claim (PVC) name"], + "type": "string" + }, + "volume": { + "title": "Volume", + "description": "Information about the actual volume instance allocated to the workspace.", + "$comment": "The actual volume allocated may be different than the request.", + "examples": ["see https://kubernetes.io/docs/concepts/storage/persistent-volumes/"], + "$ref": "#/definitions/volume" + }, "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"} + "type": "array", + "title": "Properties", + "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, + } + }, + "volume": { + "title": "Volume", + "description": "An identifiable, logical unit of data storage tied to a physical device.", + "type": "object", + "additionalProperties": false, + "properties": { + "uid": { + "title": "Unique Identifier (UID)", + "description": "The unique identifier for the volume instance within its deployment context.", + "type": "string" + }, + "name": { + "title": "Name", + "description": "The name of the volume instance", + "type": "string" + }, + "mode": { + "title": "Mode", + "description": "The mode for the volume instance.", + "type": "string", + "enum": [ + "filesystem", "block" + ], + "default": "filesystem" + }, + "path": { + "title": "Path", + "description": "The underlying path created from the actual volume.", + "type": "string" + }, + "sizeAllocated": { + "title": "Size allocated", + "description": "The allocated size of the volume accessible to the associated workspace. This should include the scalar size as well as IEC standard unit in either decimal or binary form.", + "examples": ["10GB", "2Ti", "1Pi"], + "type": "string" + }, + "persistent": { + "title": "Persistent", + "description": "Indicates if the volume persists beyond the life of the resource it is associated with.", + "type": "boolean" + }, + "remote": { + "title": "Remote", + "description": "Indicates if the volume is remotely (i.e., network) attached.", + "type": "boolean" + }, "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." + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" } } - }, - "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, + } + }, + "trigger": { + "title": "Trigger", + "description": "Represents a resource that can conditionally activate (or fire) tasks based upon associated events and their data.", + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "bom-ref", + "uid" + ], + "properties": { + "bom-ref": { + "title": "BOM Reference", + "description": "An optional identifier which can be used to reference the trigger elsewhere in the BOM. Every bom-ref MUST be unique within the BOM.", + "$ref": "#/definitions/refType" + }, + "uid": { + "title": "Unique Identifier (UID)", + "description": "The unique identifier for the resource instance within its deployment context.", + "type": "string" + }, + "name": { + "title": "Name", + "description": "The name of the resource instance.", + "type": "string" + }, + "description": { + "title": "Description", + "description": "A description of the resource instance.", + "type": "string" + }, + "resourceReferences": { + "title": "Resource references", + "description": "References to component or service resources that are used to realize the resource instance.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/resourceReferenceChoice" + } + }, + "type": { + "title": "Type", + "description": "The source type of event which caused the trigger to fire.", + "type": "string", + "enum": [ + "manual", + "api", + "webhook", + "scheduled" + ] + }, + "event": { + "title": "Event", + "description": "The event data that caused the associated trigger to activate.", + "$ref": "#/definitions/event" + }, + "conditions": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/condition" + } + }, + "timeActivated": { + "title": "Time activated", + "description": "The date and time (timestamp) when the trigger was activated.", + "type": "string", + "format": "date-time" + }, + "inputs": { + "title": "Inputs", + "description": "Represents resources and data brought into a task at runtime by executor or task commands", + "examples": ["a `configuration` file which was declared as a local `component` or `externalReference`"], + "type": "array", + "items": { + "$ref": "#/definitions/inputType" + }, + "uniqueItems": true + }, + "outputs": { + "title": "Outputs", + "description": "Represents resources and data output from a task at runtime by executor or task commands", + "examples": ["a log file or metrics data produced by the task"], + "type": "array", + "items": { + "$ref": "#/definitions/outputType" + }, + "uniqueItems": true + }, "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" + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" } } - }, - "vulnerabilitySource": { - "type": "object", - "title": "Source", - "description": "The source of vulnerability information. This is often the organization that published the vulnerability.", - "additionalProperties": false, + } + }, + "event": { + "title": "Event", + "description": "Represents something that happened that may trigger a response.", + "type": "object", + "additionalProperties": false, + "properties": { + "uid": { + "title": "Unique Identifier (UID)", + "description": "The unique identifier of the event.", + "type": "string" + }, + "description": { + "title": "Description", + "description": "A description of the event.", + "type": "string" + }, + "timeReceived": { + "title": "Time Received", + "description": "The date and time (timestamp) when the event was received.", + "type": "string", + "format": "date-time" + }, + "data": { + "title": "Data", + "description": "Encoding of the raw event data.", + "$ref": "#/definitions/attachment" + }, + "source": { + "title": "Source", + "description": "References the component or service that was the source of the event", + "$ref": "#/definitions/resourceReferenceChoice" + }, + "target": { + "title": "Target", + "description": "References the component or service that was the target of the event", + "$ref": "#/definitions/resourceReferenceChoice" + }, "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" - ] + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" } } - }, - "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" - } + } + }, + "inputType": { + "title": "Input type", + "description": "Type that represents various input data types and formats.", + "type": "object", + "oneOf": [ + { + "required": [ + "resource" + ] + }, + { + "required": [ + "parameters" + ] + }, + { + "required": [ + "environmentVars" + ] + }, + { + "required": [ + "data" + ] + } + ], + "additionalProperties": false, + "properties": { + "source": { + "title": "Source", + "description": "A references to the component or service that provided the input to the task (e.g., reference to a service with data flow value of `inbound`)", + "examples": [ + "source code repository", + "database" + ], + "$ref": "#/definitions/resourceReferenceChoice" + }, + "target": { + "title": "Target", + "description": "A reference to the component or service that received or stored the input if not the task itself (e.g., a local, named storage workspace)", + "examples": [ + "workspace", + "directory" + ], + "$ref": "#/definitions/resourceReferenceChoice" + }, + "resource": { + "title": "Resource", + "description": "A reference to an independent resource provided as an input to a task by the workflow runtime.", + "examples": [ + "reference to a configuration file in a repository (i.e., a bom-ref)", + "reference to a scanning service used in a task (i.e., a bom-ref)" + ], + "$ref": "#/definitions/resourceReferenceChoice" + }, + "parameters": { + "title": "Parameters", + "description": "Inputs that have the form of parameters with names and values.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/parameter" + } + }, + "environmentVars": { + "title": "Environment variables", + "description": "Inputs that have the form of parameters with names and values.", + "type": "array", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/property" }, - "individuals": { - "type": "array", - "title": "Individuals", - "description": "The individuals, not associated with organizations, that are credited with vulnerability discovery.", - "additionalItems": false, - "items": { - "$ref": "#/definitions/organizationalContact" - } + { + "type": "string" } - } - }, - "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" - ] - } + ] + } + }, + "data": { + "title": "Data", + "description": "Inputs that have the form of data.", + "$ref": "#/definitions/attachment" + }, + "properties": { + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" + } + } + } + }, + "outputType": { + "type": "object", + "oneOf": [ + { + "required": [ + "resource" + ] + }, + { + "required": [ + "environmentVars" + ] + }, + { + "required": [ + "data" + ] + } + ], + "additionalProperties": false, + "properties": { + "type": { + "title": "Type", + "description": "Describes the type of data output.", + "type": "string", + "enum": [ + "artifact", + "attestation", + "log", + "evidence", + "metrics", + "other" + ] + }, + "source": { + "title": "Source", + "description": "Component or service that generated or provided the output from the task (e.g., a build tool)", + "$ref": "#/definitions/resourceReferenceChoice" + }, + "target": { + "title": "Target", + "description": "Component or service that received the output from the task (e.g., reference to an artifactory service with data flow value of `outbound`)", + "examples": ["a log file described as an `externalReference` within its target domain."], + "$ref": "#/definitions/resourceReferenceChoice" + }, + "resource": { + "title": "Resource", + "description": "A reference to an independent resource generated as output by the task.", + "examples": [ + "configuration file", + "source code", + "scanning service" + ], + "$ref": "#/definitions/resourceReferenceChoice" + }, + "data": { + "title": "Data", + "description": "Outputs that have the form of data.", + "$ref": "#/definitions/attachment" + }, + "environmentVars": { + "title": "Environment variables", + "description": "Outputs that have the form of environment variables.", + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/property" }, - "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." + { + "type": "string" } - } + ] }, - "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" - } - } - } - } - } + "uniqueItems": true + }, + "properties": { + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" + } + } + } + }, + "resourceReferenceChoice": { + "title": "Resource reference choice", + "description": "A reference to a locally defined resource (e.g., a bom-ref) or an externally accessible resource.", + "$comment": "Enables reference to a resource that participates in a workflow; using either internal (bom-ref) or external (externalReference) types.", + "type": "object", + "additionalProperties": false, + "properties": { + "ref": { + "title": "BOM Reference", + "description": "References an object by its bom-ref attribute", + "anyOf": [ + { + "title": "Ref", + "$ref": "#/definitions/refLinkType" }, - "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" + { + "title": "BOM-Link Element", + "$ref": "#/definitions/bomLinkElementType" } - } + ] + }, + "externalReference": { + "title": "External reference", + "description": "Reference to an externally accessible resource.", + "$ref": "#/definitions/externalReference" } }, - "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)." + "oneOf": [ + { + "required": [ + "ref" + ] + }, + { + "required": [ + "externalReference" + ] + } + ] + }, + "condition": { + "title": "Condition", + "description": "A condition that was used to determine a trigger should be activated.", + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "title": "Description", + "description": "Describes the set of conditions which cause the trigger to activate.", + "type": "string" + }, + "expression": { + "title": "Expression", + "description": "The logical expression that was evaluated that determined the trigger should be fired.", + "type": "string" + }, + "properties": { + "type": "array", + "title": "Properties", + "items": { + "$ref": "#/definitions/property" + } + } + } + }, + "taskType": { + "type": "string", + "enum": [ + "copy", + "clone", + "lint", + "scan", + "merge", + "build", + "test", + "deliver", + "deploy", + "release", + "clean", + "other" + ] + }, + "parameter": { + "title": "Parameter", + "description": "A representation of a functional parameter.", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "description": "The name of the parameter.", + "type": "string" + }, + "value": { + "title": "Value", + "description": "The value of the parameter.", + "type": "string" + }, + "dataType": { + "title": "Data type", + "description": "The data type of the parameter.", + "type": "string" + } } + }, + "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 index a0218439f74..de633349c7e 100644 --- a/schema/cyclonedx/cyclonedx.xsd +++ b/schema/cyclonedx/cyclonedx.xsd @@ -16,13 +16,13 @@ limitations under the License. --> + version="1.5.0"> @@ -37,9 +37,49 @@ limitations under the License. - Identifier-DataType for interlinked elements. + Identifier for referable and therefore interlink-able elements. - + + + + + + + + + Descriptor for an element identified by the attribute "bom-ref" in the same BOM document. + In contrast to `bomLinkElementType`. + + + + + + + + + Descriptor for another BOM document. + See https://cyclonedx.org/capabilities/bomlink/ + + + + + + + + + + + Descriptor for an element in another BOM document. + See https://cyclonedx.org/capabilities/bomlink/ + + + + + + + + + @@ -49,14 +89,74 @@ limitations under the License. The date and time (timestamp) when the BOM was created. + + + + The product lifecycle(s) that this BOM represents. + + + + + + + + + + + + A pre-defined phase in the product lifecycle. + + + + + + + + + The name of the lifecycle phase + + + + + + + The description of the lifecycle phase + + + + + + + + + + The tool(s) used in the creation of the BOM. - - - + + + + + DEPRECATED. Use tools\components or tools\services instead. + + + + + + + A list of software and hardware components used as tools. + + + + + A list of services used as tools. + + + + @@ -89,7 +189,7 @@ limitations under the License. - Provides the ability to document properties in a key/value store. + 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. Property names of interest to the general public are encouraged to be registered in the @@ -113,6 +213,75 @@ limitations under the License. + + + + + + BOM produced early in the development lifecycle containing inventory of components and services + that are proposed or planned to be used. The inventory may need to be procured, retrieved, + or resourced prior to use. + + + + + + + BOM consisting of information obtained prior to a build process and may contain source files + and development artifacts and manifests. The inventory may need to be resolved and retrieved + prior to use. + + + + + + + BOM consisting of information obtained during a build process where component inventory is + available for use. The precise versions of resolved components are usually available at this + time as well as the provenance of where the components were retrieved from. + + + + + + + BOM consisting of information obtained after a build process has completed and the resulting + components(s) are available for further analysis. Built components may exist as the result of a + CI/CD process, may have been installed or deployed to a system or device, and may need to be + retrieved or extracted from the system or device. + + + + + + + BOM produced that represents inventory that is running and operational. This may include staging + or production environments and will generally encompass multiple SBOMs describing the applications + and operating system, along with HBOMs describing the hardware that makes up the system. Operations + Bill of Materials (OBOM) can provide full-stack inventory of runtime environments, configurations, + and additional dependencies. + + + + + + + BOM consisting of information observed through network discovery providing point-in-time + enumeration of embedded, on-premise, and cloud-native services such as server applications, + connected devices, microservices, and serverless functions. + + + + + + + BOM containing inventory that will be, or has been retired from operations. + + + + + + @@ -138,6 +307,14 @@ limitations under the License. + + + + An optional identifier which can be used to reference the object 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 @@ -219,6 +396,14 @@ limitations under the License. + + + + An optional identifier which can be used to reference the object 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 @@ -358,7 +543,7 @@ limitations under the License. - Provides the ability to document properties in a key/value store. + 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. Property names of interest to the general public are encouraged to be registered in the @@ -397,6 +582,21 @@ limitations under the License. Specifies optional release notes. + + + A model card describes the intended uses of a machine learning model and potential + limitations, including biases and ethical considerations. Model cards typically contain the + training parameters, which datasets were used to train the model, performance metrics, and other + relevant data useful for ML transparency. This object SHOULD be specified for any component of + type `machine-learning-model` and MUST NOT be specified for other component types. + + + + + This object SHOULD be specified for any component of type `data` and MUST NOT be + specified for other component types. + + @@ -463,6 +663,141 @@ limitations under the License. an externalReference should also be specified for completeness. + + + Licensing details describing the licensor/licensee, license type, renewal and + expiration dates, and other important metadata + + + + + + License identifiers that may be used to manage licenses and + their lifecycle + + + + + + + + + + The individual or organization that grants a license to another + individual or organization + + + + + + + The organization that granted the license + + + + + The individual, not associated with an organization, + that granted the license + + + + + + + + + The individual or organization for which a license was granted to + + + + + + + The organization that was granted the license + + + + + The individual, not associated with an organization, + that was granted the license + + + + + + + + + The individual or organization that purchased the license + + + + + + + The organization that purchased the license + + + + + The individual, not associated with an organization, + that purchased the license + + + + + + + + + The purchase order identifier the purchaser sent to a supplier or + vendor to authorize a purchase + + + + + The type of license(s) that was granted to the licensee + + + + + + + + + + The timestamp indicating when the license was last + renewed. For new purchases, this is often the purchase or acquisition date. + For non-perpetual licenses or subscriptions, this is the timestamp of when the + license was last renewed. + + + + + The timestamp indicating when the current license + expires (if applicable). + + + + + + Allows any undeclared elements as long as the elements are placed in a different namespace. + + + + + + + + + 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. 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. + + @@ -471,6 +806,14 @@ limitations under the License. + + + + An optional identifier which can be used to reference the license elsewhere in the BOM. + Uniqueness is enforced within all elements and children of the root-level bom element. + + + @@ -565,6 +908,12 @@ limitations under the License. virtualization technology. Refer to https://en.wikipedia.org/wiki/OS-level_virtualization + + + A runtime environment which interprets or executes software. This may include + runtimes such as those that execute bytecode or low-code/no-code application platforms. + + A software operating system without regard to deployment model @@ -577,7 +926,15 @@ limitations under the License. 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. + information about the software running on the device. + See also the list of known device properties: https://github.com/CycloneDX/cyclonedx-property-taxonomy/blob/main/cdx/device.md + + + + + + A special type of software that operates or controls a particular type of device. + Refer to https://en.wikipedia.org/wiki/Device_driver @@ -592,6 +949,17 @@ limitations under the License. for information about files. + + + A model based on training data that can make predictions or decisions without + being explicitly programmed to do so. + + + + + A collection of discrete values that convey information. + + @@ -612,6 +980,108 @@ limitations under the License. + + + + + A license that grants use of software solely for the purpose + of education or research. + + + + + A license covering use of software embedded in a specific + piece of hardware. + + + + + A Client Access License (CAL) allows client computers to access + services provided by server software. + + + + + A Concurrent User license (aka floating license) limits the + number of licenses for a software application and licenses are shared among + a larger number of users. + + + + + A license where the core of a computer's processor is assigned + a specific number of points. + + + + + A license for which consumption is measured by non-standard + metrics. + + + + + A license that covers a defined number of installations on + computers and other types of devices. + + + + + A license that grants permission to install and use software + for trial purposes. + + + + + A license that grants access to the software to one or more + pre-defined users. + + + + + A license that grants access to the software on one or more + pre-defined computers or devices. + + + + + An Original Equipment Manufacturer license that is delivered + with hardware, cannot be transferred to other hardware, and is valid for the + life of the hardware. + + + + + A license where the software is sold on a one-time basis and + the licensee can use a copy of the software indefinitely. + + + + + A license where each installation consumes points per + processor. + + + + + A license where the licensee pays a fee to use the software + or service. + + + + + A license that grants access to the software or service by a + specified number of users. + + + + + Another license type. + + + + + @@ -724,7 +1194,7 @@ limitations under the License. - Bill-of-material document (CycloneDX, SPDX, SWID, etc) + Bill-of-materials (SBOM, OBOM, HBOM, SaaSBOM, etc) @@ -757,6 +1227,11 @@ limitations under the License. Direct or repository download location + + + The location where a component was published to. This is often the same as "distribution" but may also include specialized publishing processes that act as an intermediary + + The URL to the license file. If a license URL has been defined in the license @@ -778,38 +1253,159 @@ limitations under the License. URL to release notes - + - Use this if no other types accurately describe the purpose of the external reference + Specifies a way to contact the maintainer, supplier, or provider in the event of a security incident. Common URIs include links to a disclosure procedure, a mailto (RFC-2368) that specifies an email address, a tel (RFC-3966) that specifies a phone number, or dns (RFC-4501]) that specifies the records containing DNS Security TXT. - - - - - - - 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 + A model card describes the intended uses of a machine learning model, potential + limitations, biases, ethical considerations, training parameters, datasets used to train the + model, performance metrics, and other relevant data useful for ML transparency. - - - - - - - + + - The URL to the external reference + A record of events that occurred in a computer system or application, such as problems, errors, or information on current operations. - - + + + + Parameters or settings that may be used by other components or services. + + + + + Information used to substantiate a claim. + + + + + Describes how a component or service was manufactured or deployed. + + + + + Human or machine-readable statements containing facts, evidence, or testimony + + + + + An enumeration of identified weaknesses, threats, and countermeasures, dataflow diagram (DFD), attack tree, and other supporting documentation in human-readable or machine-readable format + + + + + The defined assumptions, goals, and capabilities of an adversary. + + + + + Identifies and analyzes the potential of future events that may negatively impact individuals, assets, and/or the environment. Risk assessments may also include judgments on the tolerability of each risk. + + + + + A Vulnerability Disclosure Report (VDR) which asserts the known and previously unknown vulnerabilities that affect a component, service, or product including the analysis and findings describing the impact (or lack of impact) that the reported vulnerability has on a component, service, or product. + + + + + A Vulnerability Exploitability eXchange (VEX) which asserts the known vulnerabilities that do not affect a product, product family, or organization, and optionally the ones that do. The VEX should include the analysis and findings describing the impact (or lack of impact) that the reported vulnerability has on the product, product family, or organization. + + + + + Results from an authorized simulated cyberattack on a component or service, otherwise known as a penetration test + + + + + SARIF or proprietary machine or human-readable report for which static analysis has identified code quality, security, and other potential issues with the source code + + + + + Dynamic analysis report that has identified issues such as vulnerabilities and misconfigurations + + + + + Report generated by analyzing the call stack of a running application + + + + + Report generated by Software Composition Analysis (SCA), container analysis, or other forms of component analysis + + + + + Report containing a formal assessment of an organization, business unit, or team against a maturity model + + + + + Industry, regulatory, or other certification from an accredited (if applicable) certification body + + + + + Report or system in which quality metrics can be obtained + + + + + Code or configuration that defines and provisions virtualized infrastructure, commonly referred to as Infrastructure as Code (IaC) + + + + + Plans of Action and Milestones (POAM) compliment an "attestation" external reference. POAM is defined by NIST as a "document that identifies tasks needing to be accomplished. It details resources required to accomplish the elements of the plan, any milestones in meeting the tasks and scheduled completion dates for the milestones". + + + + + 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 are not included with the BOM. They may also establish specific relationships + within or external to the BOM. + + + + + + Zero or more external references can be defined + + + + + + + + + + The URI (URL or URN) to the external reference. External references + are URIs and therefore can accept any URL scheme including https, mailto, tel, and dns. + External references may also include formally registered URNs such as CycloneDX BOM-Link to + reference CycloneDX BOMs or any object within a BOM. BOM-Link transforms applicable external + references into relationships that can be expressed in a BOM or across BOMs. Refer to: + https://cyclonedx.org/capabilities/bomlink/ + + + + + + An optional comment describing the external reference @@ -1188,9 +1784,9 @@ limitations under the License. - + - References a component or service by the its bom-ref attribute + References a component or service by its bom-ref attribute @@ -1205,10 +1801,12 @@ limitations under the License. - 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. + Defines the direct dependencies of a component or service. Components or services + that do not have their own dependencies MUST be declared as empty elements within the graph. + Components or services 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 object being dependency-free. It is RECOMMENDED to leverage compositions to + indicate unknown dependency graphs. @@ -1288,15 +1886,85 @@ limitations under the License. A value of false indicates that by using the service, a trust boundary is not crossed. + + + The name of the trust zone the service resides in. + + - - + + + + + DEPRECATED: Specifies the data classification. THIS FIELD IS DEPRECATED AS OF v1.5. Use dataflow\classification instead + + + + Specifies the data classification. + + + + + Specifies the data classification. + + + + + + The URI, URL, or BOM-Link of the components or services the data came in from. + + + + + + + + + + + + + + The URI, URL, or BOM-Link of the components or services the data is sent to. + + + + + + + + + + + + + + + + Name for the defined data. + + + + + + + Short description of the data content and usage. + + + + + + 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. + + + - + @@ -1307,7 +1975,7 @@ limitations under the License. - Provides the ability to document properties in a key/value store. + 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. Property names of interest to the general public are encouraged to be registered in the @@ -1398,11 +2066,25 @@ limitations under the License. - + A valid SPDX license expression. Refer to https://spdx.org/specifications for syntax requirements + + + + + + + An optional identifier which can be used to reference the license elsewhere in the BOM. + Uniqueness is enforced within all elements and children of the root-level bom element. + + + + + + @@ -1413,8 +2095,208 @@ limitations under the License. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Evidence that substantiates the identity of a component. + + + + + + The identity field of the component which the evidence describes. + + + + + The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. + + + + + The methods used to extract and/or analyze the evidence. + + + + + + + + + The technique used in this method of analysis. + + + + + The confidence of the evidence from 0 - 1, where 1 is 100% confidence. Confidence is specific to the technique used. Each technique of analysis can have independent confidence. + + + + + The value or contents of the evidence. + + + + + + + + + + + + The object in the BOM identified by its bom-ref. This is often a component or service, + but may be any object type supporting bom-refs. Tools used for analysis should already + be defined in the BOM, either in the metadata/tools, components, or formulation. + + + + + + + + + + + + + + Evidence of individual instances of a component spread across multiple locations. + + + + + + + + + The location or path to where the component was found. + + + + + + + An optional identifier which can be used to reference the occurrence elsewhere + in the BOM. Every bom-ref MUST be unique within the BOM. + + + + + + + + + + + Evidence of the components use through the callstack. + + + + + + + + + + + + A package organizes modules into namespaces, providing a unique namespace for each type it contains. + + + + + A module or class that encloses functions/methods and other code. + + + + + A block of code designed to perform a particular task. + + + + + Optional arguments that are passed to the module or function. + + + + + + + + + + The line number the code that is called resides on. + + + + + The column the code that is called resides. + + + + + The full path and filename of the module. + + + + + + + + + + + + The object in the BOM identified by its bom-ref. This is often a component or service, + but may be any object type supporting bom-refs. Tools used for analysis should already + be defined in the BOM, either in the metadata/tools, components, or formulation. + + + + + + + + + + + @@ -1502,14 +2384,41 @@ limitations under the License. + + + + The bom-ref identifiers of the vulnerabilities being described. + + + + + + + + + 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 composition elsewhere in the BOM. + Uniqueness is enforced within all elements and children of the root-level bom element. + + + - The relationship is complete. No further relationships including constituent components, services, or dependencies exist. + The relationship is complete. No further relationships including constituent components, services, or dependencies are known to exist. @@ -1522,11 +2431,31 @@ limitations under the License. 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, limited specifically to those that are proprietary. + + + + + The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented, limited specifically to those that are opensource. + + The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented. + + + The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented, limited specifically to those that are proprietary. + + + + + The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented, limited specifically to those that are opensource. + + 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. @@ -1566,9 +2495,9 @@ limitations under the License. * 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. + 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. + by the project or manufacturer that produced it. @@ -1603,7 +2532,7 @@ limitations under the License. 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). + include unofficial terms used by development and marketing teams (e.g. code names). @@ -1636,7 +2565,7 @@ limitations under the License. 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. + note elements may be specified to support release notes in a wide variety of languages. @@ -1659,7 +2588,7 @@ limitations under the License. - Provides the ability to document properties in a key/value store. + 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. Property names of interest to the general public are encouraged to be registered in the @@ -1683,23 +2612,1986 @@ limitations under the License. - - - - 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. - - - - - - - + + + + + A model card describes the intended uses of a machine learning model and potential limitations, including + biases and ethical considerations. Model cards typically contain the training parameters, which datasets + were used to train the model, performance metrics, and other relevant data useful for ML transparency. + This object SHOULD be specified for any component of type `machine-learning-model` and MUST NOT be specified + for other component types. + + + + + + + Hyper-parameters for construction of the model. + + + + + + + + The overall approach to learning used by the model for problem solving. + + + + + + + + Learning types describing the learning problem or hybrid learning problem. + + + + + + + + + + Directly influences the input and/or output. Examples include classification, + regression, clustering, etc. + + + + + + + The model architecture family such as transformer network, convolutional neural + network, residual neural network, LSTM neural network, etc. + + + + + + + The specific architecture of the model such as GPT-1, ResNet-50, YOLOv3, etc. + + + + + + + The datasets used to train and evaluate the model. + + + + + + + References a data component by the components bom-ref attribute + + + + + + + + + + + + + The input format(s) of the model + + + + + + + + + + + The data format for input to the model. Example formats include string, image, time-series + + + + + + + + + + + + + The output format(s) from the model + + + + + + + + + + + The data format for output from the model. Example formats include string, image, time-series + + + + + + + + + + + + + + + + A quantitative analysis of the model + + + + + + + + + + + + + + The type of performance metric. + + + + + + + The value of the performance metric. + + + + + + + The name of the slice this metric was computed on. By default, assume + this metric is not sliced. + + + + + + + The confidence interval of the metric. + + + + + + + + The lower bound of the confidence interval. + + + + + + + The upper bound of the confidence interval. + + + + + + + + + + + + + + + + A collection of graphics that represent various measurements + + + + + + + + A description of this collection of graphics. + + + + + + + A collection of graphics. + + + + + + + + + + + The name of the graphic. + + + + + + + The graphic (vector or raster). Base64 encoding MUST be specified for binary images. + + + + + + + + + + + + + + + + + + + What considerations should be taken into account regarding the model's construction, training, + and application? + + + + + + + + Who are the intended users of the model? + + + + + + + + + + + + What are the intended use cases of the model? + + + + + + + + + + + + What are the known technical limitations of the model? E.g. What kind(s) of data + should the model be expected not to perform well on? What are the factors that might + degrade model performance? + + + + + + + + + + + + What are the known tradeoffs in accuracy/performance of the model? + + + + + + + + + + + + What are the ethical (or environmental) risks involved in the application of this model? + + + + + + + + + + + The name of the risk + + + + + + + Strategy used to address this risk + + + + + + + + + + + + + How does the model affect groups at risk of being systematically disadvantaged? + What are the harms and benefits to the various affected groups? + + + + + + + + + + + The groups or individuals at risk of being systematically disadvantaged by the model. + + + + + + + Expected benefits to the identified groups. + + + + + + + Expected harms to the identified groups. + + + + + + + With respect to the benefits and harms outlined, please + describe any mitigation strategy implemented. + + + + + + + + + + + + + + + + + An optional identifier which can be used to reference the model card elsewhere in the BOM. + Every bom-ref MUST be unique within the BOM. + + + + + + + + + + TODO + + + + + TODO + + + + + TODO + + + + + TODO + + + + + TODO + + + + + + + + + + + The general theme or subject matter of the data being specified. + + + + + + + The name of the dataset. + + + + + + + The contents or references to the contents of the data being described. + + + + + + + An optional way to include textual or encoded data. + + + + + The URL to where the data can be retrieved. + + + + + Provides the ability to document name-value parameters used for configuration. + + + + + + + + + Data classification tags data according to its type, sensitivity, and value if altered, stolen, or destroyed. + + + + + + + A description of any sensitive data in a dataset. + + + + + + + A collection of graphics that represent various measurements. + + + + + + + A description of the dataset. Can describe size of dataset, whether it's used for source code, + training, testing, or validation, etc. + + + + + + + + + An optional identifier which can be used to reference the dataset elsewhere in the BOM. + Every bom-ref MUST be unique within the BOM. + + + + + + + + + + + Data custodians are responsible for the safe custody, transport, and storage of data. + + + + + + + + + + + + Data stewards are responsible for data content, context, and associated business rules. + + + + + + + + + + + + Data owners are concerned with risk and appropriate access to data. + + + + + + + + + + + + + + + + + + + + + + A collection of graphics that represent various measurements. + + + + + + + A description of this collection of graphics. + + + + + + + A collection of graphics. + + + + + + + + + + + The name of the graphic. + + + + + + + The graphic (vector or raster). Base64 encoding MUST be specified for binary images. + + + + + + + + + + + + + + + + + Any type of code, code snippet, or data-as-code. + + + + + Parameters or settings that may be used by other components. + + + + + A collection of data. + + + + + Data that can be used to create new instances of what the definition defines. + + + + + Any other type of data that does not fit into existing definitions. + + + + + + + + + References a component or service by 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 a 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 information useful in understanding root cause. + + + + + Recommendations of how the vulnerability can be remediated or mitigated. + + + + + A bypass, usually temporary, of the vulnerability that reduces its likelihood and/or impact. Workarounds often involve changes to configuration or deployments. + + + + + + + Evidence used to reproduce the vulnerability. + + + + + + Precise steps to reproduce the vulnerability. + + + + + A description of the environment in which reproduction was possible. + + + + + Supporting material that helps in reproducing or understanding how reproduction is possible. This may include screenshots, payloads, and PoC exploit code. + + + + + + + + + + + + + + + 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. + + + + + The date and time (timestamp) when the vulnerability record was rejected (if applicable). + + + + + 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. + + + + + + + DEPRECATED. Use tools\components or tools\services instead. + + + + + + + A list of software and hardware components used as tools. + + + + + A list of services used as tools. + + + + + + + + + + + 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 date and time (timestamp) when the analysis was first issued. + + + + + + + The date and time (timestamp) when the analysis was last updated. + + + + + + + + + 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 name/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. + + + + + + + + + + + + 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 created the annotation + + + + + The person that created the annotation + + + + + The tool or component that created the annotation + + + + + The service that created the annotation + + + + + + + + + + + The objects in the BOM identified by their bom-ref's. This is often components or services, but may be any object type supporting bom-refs. + + + + + + + + + Allows any undeclared elements as long as the elements are placed in a different namespace. + + + + + + + + + The organization, individual, component, or service which created the textual content + of the annotation. + + + + + The date and time (timestamp) when the annotation was created. + + + + + The textual content of the annotation. + + + + + + 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 annotation 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. + + + + + + + + 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 CVSS v4.0 standard + https://www.first.org/cvss/v4-0/ + + + + + + + The rating is based on OWASP Risk Rating + https://owasp.org/www-community/OWASP_Risk_Rating_Methodology + + + + + + + The rating is based on Stakeholder Specific Vulnerability Categorization (all versions) + https://github.com/CERTCC/SSVC + + + + + + + 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. + + + + + + + + + + + + + Describes how a component or service was manufactured or deployed. This is achieved through the use + of formulas, workflows, tasks, and steps, which declare the precise steps to reproduce along with the + observed formulas describing the steps which transpired in the manufacturing process. + + + + + + + + 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. + + + + + + + + Describes workflows and resources that captures rules and other aspects of how the associated + BOM component or service was formed. + + + + + + Transient components that are used in tasks that constitute one or more of + this formula's workflows + + + + + Transient services that are used in tasks that constitute one or more of + this formula's workflows + + + + + List of workflows that can be declared to accomplish specific orchestrated goals + and independently triggered. + + + + + 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. 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 formula 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. + + + + + + + + + + + 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 unique identifier for the resource instance within its deployment context. + + + + + + + The name of the resource instance. + + + + + + + The description of the resource instance. + + + + + + References to component or service resources that are used to realize + the resource instance. + + + + + The tasks that comprise the workflow. + + + + + The graph of dependencies between tasks within the workflow. + + + + + Indicates the types of activities performed by the set of workflow tasks. + + + + + + + + + + The trigger that initiated the task. + + + + + + The sequence of steps for the task. + + + + + + + + + + + Represents resources and data brought into a task at runtime by executor + or task commands + + + + + + + + + + Represents resources and data output from a task at runtime by executor + or task commands + + + + + + + + + + + The date and time (timestamp) when the task started. + + + + + + + The date and time (timestamp) when the task ended. + + + + + + A set of named filesystem or data resource shareable by workflow tasks. + + + + + A graph of the component runtime topology for workflow's instance. + A description of the runtime component and service topology. This can describe a partial or + complete topology used to host and execute the task (e.g., hardware, operating systems, + configurations, etc.) + + + + + 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. 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. + + + + + + + + An optional identifier which can be used to reference the workflow 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. + + + + + + + + + + + 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 an object by its bom-ref attribute + + + + + + + + + + Reference to an externally accessible resource. + + + + + + + + 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 unique identifier for the resource instance within its deployment context. + + + + + + + The name of the resource instance. + + + + + + + The description of the resource instance. + + + + + + + References to component or service resources that are used to realize the resource instance. + + + + + + + Indicates the types of activities performed by the set of workflow tasks. + + + + + + + + + + + + The trigger that initiated the task. + + + + + + + The sequence of steps for the task. + + + + + + + + + + + + Represents resources and data brought into a task at runtime by executor or task commands. + + + + + + + + + + + + Represents resources and data output from a task at runtime by executor or task commands + + + + + + + + + + + + The date and time (timestamp) when the task started. + + + + + + + The date and time (timestamp) when the task ended. + + + + + + + A set of named filesystem or data resource shareable by workflow tasks. + + + + + + + A graph of the component runtime topology for task's instance. + + + + + + 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. 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. + + + + + + + + An optional identifier which can be used to reference the task 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. + + + + + + + + + + + + + + + + + + + + + + + + @@ -1716,26 +4608,99 @@ limitations under the License. - + - Specifies an individual property with a name and value. + + A named filesystem or data resource shareable by workflow tasks. + - - - - - The name of the property. Duplicate names are allowed, each potentially having a different value. - - - - - - - - + + + + The unique identifier for the resource instance within its deployment context. + + + + + + + The name of the resource instance. + + + + + + + The names for the workspace as referenced by other workflow tasks. Effectively, a name mapping + so other tasks can use their own local name in their steps. + + + + + + + + + + + + The description of the resource instance. + + + + + + + References to component or service resources that are used to realize the resource instance. + + + + + + + Describes the read-write access control for the workspace relative to the owning resource instance. + + + + + + + A path to a location on disk where the workspace will be available to the associated task's steps. + + + + + + + The name of a domain-specific data type the workspace represents. This property is for CI/CD + frameworks that are able to provide access to structured, managed data at a more granular level + than a filesystem. + + + + - Defines a weakness in an component or service that could be exploited or triggered by a threat source. + + Identifies the reference to the request for a specific volume type and parameters. + + + + + + + Information about the actual volume instance allocated to the workspace. + + + + + + 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. 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. @@ -1746,6 +4711,14 @@ limitations under the License. + + + + An optional identifier which can be used to reference the workflow 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 @@ -1754,259 +4727,141 @@ limitations under the License. - - - - - 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. - - - - - - - + + + + + + + + + + + + + + An identifiable, logical unit of data storage tied to a physical device. + + + + - List of vulnerability ratings. + + The unique identifier for the volume instance within its deployment context. + - - - - - - - - - - - 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. + + The name of the volume instance + - + - 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. + + The mode for the volume instance. + - + - Recommendations of how the vulnerability can be remediated or mitigated. + + The underlying path created from the actual volume. + - - - - - Published advisories of the vulnerability if provided. - - - - - - - - + - The date and time (timestamp) when the vulnerability record was created in the vulnerability database. + + The allocated size of the volume accessible to the associated workspace. This should include + the scalar size as well as IEC standard unit in either decimal or binary form. + - + - The date and time (timestamp) when the vulnerability record was first published. + + Indicates if the volume persists beyond the life of the resource it is associated with. + - + - The date and time (timestamp) when the vulnerability record was last updated. + + Indicates if the volume is remotely (i.e., network) attached. + - + - Individuals or organizations credited with the discovery of the vulnerability. + 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. 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. - - - - - The organizations credited with vulnerability discovery. - - - - - - - - - - The individuals, not associated with organizations, that are credited with vulnerability discovery. - - - - - - - - - - + + + + + + + + + + + + + + Executes specific commands or tools in order to accomplish its owning task as part of a sequence. + + + + - The tool(s) used to identify, confirm, or score the vulnerability. + + A name for the step. + - - - - - - - - - - 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. - - - - - + + + + A description of the step. + + - + - The components or services that are affected by the vulnerability. + + Ordered list of commands or directives for the step + - - + + - - + + - References a component or service by the objects bom-ref. + + A text representation of the executed command. + - + - Zero or more individual versions or range of versions. + 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. 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 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. - - - - - - - - @@ -2016,7 +4871,7 @@ limitations under the License. - Provides the ability to document properties in a key/value store. + 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. Property names of interest to the general public are encouraged to be registered in the @@ -2024,311 +4879,486 @@ limitations under the License. Formal registration is OPTIONAL. + + + + 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 vulnerability 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. - + - - - + + + - The name of the source. - For example: NVD, National Vulnerability Database, OSS Index, VulnDB, and GitHub Advisories + + The unique identifier for the resource instance within its deployment context. - + - The url of the vulnerability documentation as provided by the source. - For example: https://nvd.nist.gov/vuln/detail/CVE-2021-39182 + + The name of the resource instance. + - - - - - - + - The source that calculated the severity or risk rating of the vulnerability. + + The description of the resource instance. + - + - The numerical score of the rating. + + References to component or service resources that are used to realize the resource instance. + - + - Textual representation of the severity that corresponds to the numerical score of the rating. + + The source type of event which caused the trigger to fire. + - + - The risk scoring methodology/standard used. + + The event data that caused the associated trigger to activate. + - + + + + + + + A condition that was used to determine a trigger should be activated. + + + + + + + + Describes the set of conditions which cause the trigger to activate. + + + + + + + The logical expression that was evaluated that determined the trigger should be fired. + + + + + + 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. 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. + + + + + + + + + - Textual representation of the metric values used to score the vulnerability. + + The date and time (timestamp) when the trigger was activated. + - + - An optional reason for rating the vulnerability as it was. + + Represents resources and data brought into a task at runtime by executor or task commands + + + + + + - - - - - - + - An optional name of the advisory. + + Represents resources and data output from a task at runtime by executor or task commands + + + + + + - + - Location where the advisory can be obtained. + 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. 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. + + + + + + + An optional identifier which can be used to reference the trigger 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. + + - - - - 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 unique identifier of the event. - - + + - 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). + A description of the event. - - + + - The vulnerability may be directly or indirectly exploitable. + The date and time (timestamp) when the event was received. - - + + + + + Encoding of the raw event data. + + + + - The vulnerability is being investigated. + References the component or service that was the source of the event - - + + - The vulnerability is not specific to the component or service and was falsely identified or associated. + References the component or service that was the target of the event - - + + + + 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. 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. + + + - The component or service is not affected by the vulnerability. Justification should be specified - for all not_affected cases. + 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 rationale of why the impact analysis state was asserted. + + Type that represents various input data types and formats. - - + + + + + + A reference to an independent resource provided as an input to a task by the workflow runtime. + + + + + + + Inputs that have the form of parameters with names and values. + + + + + + + Inputs that have the form of parameters with names and values. + + + + + + + + + + + + + + + + Inputs that have the form of data. + + + + + - The code has been removed or tree-shaked. + A references to the component or service that provided the input to the task + (e.g., reference to a service with data flow value of inbound) - - + + - The vulnerable code is not invoked at runtime. + A reference to the component or service that received or stored the input if not the task + itself (e.g., a local, named storage workspace) - - + + - - Exploitability requires a configurable option to be set/unset. - + 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. 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. - - + + - Exploitability requires a dependency that is not present. + 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. + + + + + + + + Represents resources and data output from a task at runtime by executor or task commands + + + + + + + + A reference to an independent resource generated as output by the task. + + + + + + + Outputs that have the form of environment variables. + + + + + + + + + + + + + + + + Outputs that have the form of data. + + + + + - Exploitability requires a certain environment which is not present. + Describes the type of data output. - - + + - Exploitability requires a compiler flag to be set/unset. + Component or service that generated or provided the output from the task (e.g., a build tool) - - + + - Exploits are prevented at runtime. + Component or service that received the output from the task + (e.g., reference to an artifactory service with data flow value of outbound) - - + + - - Attacks are blocked at physical, logical, or network perimeter. - + 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. 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. - - + + - Preventative measures have been implemented that reduce the likelihood and/or impact of the vulnerability. + 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 the severity or risk scoring methodology or standard used. + + A representation of a functional parameter. - - - - - 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 name of the parameter. - - + + - - The rating is based on CVSS v3.1 standard - https://www.first.org/cvss/v3-1/ + + The value of the parameter. - - + + - - The rating is based on OWASP Risk Rating - https://owasp.org/www-community/OWASP_Risk_Rating_Methodology + + The data type of the parameter. - - + + - - Use this if the risk scoring methodology is not based on any of the options above + + Allows any undeclared elements as long as the elements are placed in a different namespace. - - - - - - - - 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. - - - - - - - - - + + + + + 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. + + + @@ -2361,12 +5391,12 @@ limitations under the License. - Compositions describe constituent parts (including components, services, and dependency relationships) and their completeness. + Compositions describe constituent parts (including components, services, and dependency relationships) and their completeness. The completeness of vulnerabilities expressed in a BOM may also be described. - Provides the ability to document properties in a key/value store. + 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. Property names of interest to the general public are encouraged to be registered in the @@ -2379,6 +5409,23 @@ limitations under the License. Vulnerabilities identified in components or services. + + + Comments made by people, organizations, or tools about any object with + a bom-ref, such as components, services, vulnerabilities, or the BOM itself. Unlike + inventory information, annotations may contain opinion or commentary from various + stakeholders. Annotations may be inline (with inventory) or externalized via BOM-Link, + and may optionally be signed. + + + + + Describes how a component or service was manufactured or deployed. This is + achieved through the use of formulas, workflows, tasks, and steps, which declare the precise + steps to reproduce along with the observed formulas describing the steps which transpired + in the manufacturing process. + + @@ -2387,7 +5434,7 @@ limitations under the License. - + 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 diff --git a/schema/cyclonedx/spdx.xsd b/schema/cyclonedx/spdx.xsd index 66ba6f199e7..c2126601965 100644 --- a/schema/cyclonedx/spdx.xsd +++ b/schema/cyclonedx/spdx.xsd @@ -2,79 +2,84 @@ + version="1.0-3.21"> - + - Interbase Public License v1.0 + BSD Zero Clause License - + - Mup License + Attribution Assurance License - + - GNU General Public License v2.0 w/Autoconf exception + Abstyles License - + - Open LDAP Public License v2.1 + AdaCore Doc License - + - Creative Commons Attribution Non Commercial Share Alike 3.0 IGO + Adobe Systems Incorporated Source Code License Agreement - + - GNU Library General Public License v2 or later + Adobe Glyph List License - + - XPP License + Amazon Digital Services License - + - SIL Open Font License 1.1 + Academic Free License v1.1 - + - CNRI Python License + Academic Free License v1.2 - + - Linux man-pages Copyleft + Academic Free License v2.0 - + - Open LDAP Public License v2.2 + Academic Free License v2.1 - + - Open Software License 1.1 + Academic Free License v3.0 - + - Eclipse Public License 2.0 + Afmparse License - + - Academic Free License v1.1 + Affero General Public License v1.0 + + + + + Affero General Public License v1.0 only @@ -82,709 +87,789 @@ Affero General Public License v1.0 or later - + - Good Luck With That Public License + GNU Affero General Public License v3.0 - + - MIT License Modern Variant + GNU Affero General Public License v3.0 only - + - BSD 1-Clause License + GNU Affero General Public License v3.0 or later - + - SGI Free Software License B v1.0 + Aladdin Free Public License - + - Open Market License + AMD's plpa_map.c License - + - psfrag License + Apple MIT License - + - Artistic License 1.0 + Academy of Motion Picture Arts and Sciences BSD - + - Creative Commons Public Domain Dedication and Certification + ANTLR Software Rights Notice - + - eGenix.com Public License 1.1.0 + ANTLR Software Rights Notice with license fallback - + - European Union Public License 1.1 + Apache License 1.0 - + - Sendmail License + Apache License 1.1 - + - Python Software Foundation License 2.0 + Apache License 2.0 - + - Open Government Licence v1.0 + Adobe Postscript AFM License - + - Matrix Template Library License + Adaptive Public License 1.0 - + - Nara Institute of Science and Technology License (2003) + App::s2p License - + - ANTLR Software Rights Notice with license fallback + Apple Public Source License 1.0 - + - PostgreSQL License + Apple Public Source License 1.1 - + - Open Software License 1.0 + Apple Public Source License 1.2 - + - Nethack General Public License + Apple Public Source License 2.0 - + - Creative Commons Attribution Non Commercial No Derivatives 4.0 International + Arphic Public License - + - Code Project Open License 1.02 + Artistic License 1.0 - + - FSF Unlimited License (with License Retention) + Artistic License 1.0 w/clause 8 - + - GNU Free Documentation License v1.2 only - no invariants + Artistic License 1.0 (Perl) - + - Net-SNMP License + Artistic License 2.0 - + - Amazon Digital Services License + ASWF Digital Assets License version 1.0 - + - Sendmail License 8.23 + ASWF Digital Assets License 1.1 - + - CNRI Jython License + Baekmuk License - + - Reciprocal Public License 1.5 + Bahyph License - + - BSD-2-Clause Plus Patent License + Barr License - + - SIL Open Font License 1.1 with no Reserved Font Name + Beerware License - + - Apple Public Source License 1.2 + Bitstream Charter Font License - + - Open LDAP Public License v2.4 + Bitstream Vera Font License - + - Mozilla Public License 2.0 (no copyleft exception) + BitTorrent Open Source License v1.0 - + - ISC License + BitTorrent Open Source License v1.1 - + - Creative Commons Attribution Share Alike 2.5 Generic + SQLite Blessing - + - Sleepycat License + Blue Oak Model License 1.0.0 - + - CUA Office Public License v1.0 + Boehm-Demers-Weiser GC License - + - Frameworx Open License 1.0 + Borceux license - + - Common Public Attribution License 1.0 + Brian Gladman 3-Clause License - + - Norwegian Licence for Open Government Data (NLOD) 2.0 + BSD 1-Clause License - + - Creative Commons Attribution Non Commercial 2.0 Generic + BSD 2-Clause "Simplified" License - + - GNU Free Documentation License v1.1 or later - no invariants + BSD 2-Clause FreeBSD License - + - Creative Commons Attribution 2.5 Generic + BSD 2-Clause NetBSD License - + - Newsletr License + BSD-2-Clause Plus Patent License - + - The Parity Public License 7.0.0 + BSD 2-Clause with views sentence - + - Leptonica License + BSD 3-Clause "New" or "Revised" License - + - CMU License + BSD with attribution - + - Adobe Postscript AFM License + BSD 3-Clause Clear License - + - Creative Commons Attribution Non Commercial 2.5 Generic + Lawrence Berkeley National Labs BSD variant license - + - Cryptographic Autonomy License 1.0 (Combined Work Exception) + BSD 3-Clause Modification - + - BSD 4 Clause Shortened + BSD 3-Clause No Military License - + - Netscape Public License v1.1 + BSD 3-Clause No Nuclear License - + - Qhull License + BSD 3-Clause No Nuclear License 2014 - + - CeCILL-C Free Software License Agreement + BSD 3-Clause No Nuclear Warranty - + - GNU General Public License v1.0 only + BSD 3-Clause Open MPI variant - + - Creative Commons Attribution Non Commercial No Derivatives 3.0 Germany + BSD 4-Clause "Original" or "Old" License - + - Creative Commons Attribution Non Commercial Share Alike 3.0 Unported + BSD 4 Clause Shortened - + - Creative Commons Attribution Non Commercial Share Alike 1.0 Generic + BSD-4-Clause (University of California-Specific) - + - MIT Open Group variant + BSD 4.3 RENO License - + - Multics License + BSD 4.3 TAHOE License - + - Scheme Widget Library (SWL) Software License Agreement + BSD Advertising Acknowledgement License - + - GNU General Public License v1.0 or later + BSD with Attribution and HPND disclaimer - + - GNU General Public License v3.0 or later + BSD Protection License - + - DOC License + BSD Source Code Attribution - + - PHP License v3.0 + Boost Software License 1.0 - + - Sun Industry Standards Source License v1.2 + Business Source License 1.1 - + - Common Documentation License 1.0 + bzip2 and libbzip2 License v1.0.5 - + - Lucent Public License Version 1.0 + bzip2 and libbzip2 License v1.0.6 - + - Red Hat eCos Public License v1.1 + Computational Use of Data Agreement v1.0 - + - Licence Art Libre 1.3 + Cryptographic Autonomy License 1.0 - + - Creative Commons Attribution Share Alike 3.0 Germany + Cryptographic Autonomy License 1.0 (Combined Work Exception) - + - Community Data License Agreement Permissive 1.0 + Caldera License - + - gnuplot License + Computer Associates Trusted Open Source License 1.1 - + - App::s2p License + Creative Commons Attribution 1.0 Generic - + - iMatix Standard Function Library Agreement + Creative Commons Attribution 2.0 Generic - + - Microsoft Public License + Creative Commons Attribution 2.5 Generic - + - eCos license version 2.0 + Creative Commons Attribution 2.5 Australia - + - BSD 3-Clause "New" or "Revised" License + Creative Commons Attribution 3.0 Unported - + - Creative Commons Attribution Non Commercial No Derivatives 3.0 IGO + Creative Commons Attribution 3.0 Austria - + - ICU License + Creative Commons Attribution 3.0 Germany - + - GNU Affero General Public License v3.0 or later + Creative Commons Attribution 3.0 IGO - + - Creative Commons Attribution Share Alike 2.1 Japan + Creative Commons Attribution 3.0 Netherlands - + - Creative Commons Attribution Non Commercial Share Alike 4.0 International + Creative Commons Attribution 3.0 United States - + - The Unlicense + Creative Commons Attribution 4.0 International - + - Creative Commons Attribution Non Commercial 3.0 Germany + Creative Commons Attribution Non Commercial 1.0 Generic - + - Open LDAP Public License v1.4 + Creative Commons Attribution Non Commercial 2.0 Generic - + - CERN Open Hardware Licence Version 2 - Weakly Reciprocal + Creative Commons Attribution Non Commercial 2.5 Generic - + - SugarCRM Public License v1.1.3 + Creative Commons Attribution Non Commercial 3.0 Unported - + - IPA Font License + Creative Commons Attribution Non Commercial 3.0 Germany - + - Academic Free License v2.0 + Creative Commons Attribution Non Commercial 4.0 International - + - Unicode License Agreement - Data Files and Software (2016) + Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic - + - Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported + Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic - + - CERN Open Hardware Licence Version 2 - Permissive + Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic - + - Creative Commons Attribution Non Commercial 3.0 Unported + Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported - + - Copyfree Open Innovation License + Creative Commons Attribution Non Commercial No Derivatives 3.0 Germany - + - Cryptographic Autonomy License 1.0 + Creative Commons Attribution Non Commercial No Derivatives 3.0 IGO - + - Licence Libre du Québec – Permissive version 1.1 + Creative Commons Attribution Non Commercial No Derivatives 4.0 International - + - SIL Open Font License 1.1 with Reserved Font Name + Creative Commons Attribution Non Commercial Share Alike 1.0 Generic - + - Lucent Public License v1.02 + Creative Commons Attribution Non Commercial Share Alike 2.0 Generic - + - Open LDAP Public License v1.3 + Creative Commons Attribution Non Commercial Share Alike 2.0 Germany - + - Taiwan Open Government Data License, version 1.0 + Creative Commons Attribution-NonCommercial-ShareAlike 2.0 France - + - Creative Commons Attribution Non Commercial Share Alike 2.0 Generic + Creative Commons Attribution Non Commercial Share Alike 2.0 England and Wales - + - Python License 2.0 + Creative Commons Attribution Non Commercial Share Alike 2.5 Generic - + - NTP No Attribution + Creative Commons Attribution Non Commercial Share Alike 3.0 Unported - + - FSF All Permissive License + Creative Commons Attribution Non Commercial Share Alike 3.0 Germany - + - Erlang Public License v1.1 + Creative Commons Attribution Non Commercial Share Alike 3.0 IGO - + - Barr License + Creative Commons Attribution Non Commercial Share Alike 4.0 International - + - Creative Commons Attribution 3.0 United States + Creative Commons Attribution No Derivatives 1.0 Generic - + - BSD 3-Clause No Nuclear License 2014 + Creative Commons Attribution No Derivatives 2.0 Generic - + - No Limit Public License + Creative Commons Attribution No Derivatives 2.5 Generic - + - BSD 3-Clause Clear License + Creative Commons Attribution No Derivatives 3.0 Unported - + - SGI Free Software License B v1.1 + Creative Commons Attribution No Derivatives 3.0 Germany - + - Open Data Commons Public Domain Dedication & License 1.0 + Creative Commons Attribution No Derivatives 4.0 International - + - Common Development and Distribution License 1.0 + Creative Commons Attribution Share Alike 1.0 Generic - + - GNU Lesser General Public License v2.1 or later + Creative Commons Attribution Share Alike 2.0 Generic - + - Blue Oak Model License 1.0.0 + Creative Commons Attribution Share Alike 2.0 England and Wales - + - Creative Commons Attribution-NonCommercial-ShareAlike 2.0 France + Creative Commons Attribution Share Alike 2.1 Japan - + - Fraunhofer FDK AAC Codec Library + Creative Commons Attribution Share Alike 2.5 Generic - + - Standard ML of New Jersey License + Creative Commons Attribution Share Alike 3.0 Unported - + - Affero General Public License v1.0 only + Creative Commons Attribution Share Alike 3.0 Austria - + + + Creative Commons Attribution Share Alike 3.0 Germany + + + + + Creative Commons Attribution-ShareAlike 3.0 IGO + + + + + Creative Commons Attribution Share Alike 4.0 International + + + + + Creative Commons Public Domain Dedication and Certification + + + + + Creative Commons Zero v1.0 Universal + + + + + Common Development and Distribution License 1.0 + + + + + Common Development and Distribution License 1.1 + + + + + Common Documentation License 1.0 + + + + + Community Data License Agreement Permissive 1.0 + + + + + Community Data License Agreement Permissive 2.0 + + + + + Community Data License Agreement Sharing 1.0 + + + CeCILL Free Software License Agreement v1.0 - + - Attribution Assurance License + CeCILL Free Software License Agreement v1.1 - + - GNU General Public License v2.0 w/Font exception + CeCILL Free Software License Agreement v2.0 - + - Info-ZIP License + CeCILL Free Software License Agreement v2.1 - + - SSH OpenSSH license + CeCILL-B Free Software License Agreement - + - SSH short notice + CeCILL-C Free Software License Agreement - + - GNU General Public License v2.0 or later + CERN Open Hardware Licence v1.1 - + - Clarified Artistic License + CERN Open Hardware Licence v1.2 - + - SNIA Public License 1.1 + CERN Open Hardware Licence Version 2 - Permissive - + - GNU Free Documentation License v1.1 only - invariants + CERN Open Hardware Licence Version 2 - Strongly Reciprocal - + - BSD 3-Clause No Military License + CERN Open Hardware Licence Version 2 - Weakly Reciprocal - + - GNU Free Documentation License v1.1 + CFITSIO License - + - Mozilla Public License 1.1 + Checkmk License - + - Open LDAP Public License v1.1 + Clarified Artistic License - + - JSON License + Clips License - + - GNU Free Documentation License v1.3 only - no invariants + CMU Mach License - + - OCLC Research Public License 2.0 + CNRI Jython License - + - Open LDAP Public License v2.0.1 + CNRI Python License - + - FreeBSD Documentation License + CNRI Python Open Source GPL Compatible License Agreement - + - GNU General Public License v1.0 or later + Copyfree Open Innovation License - + - Yahoo! Public License v1.1 + Community Specification License 1.0 + + + + + Condor Public License v1.1 + + + + + copyleft-next 0.3.0 + + + + + copyleft-next 0.3.1 + + + + + Cornell Lossless JPEG License + + + + + Common Public Attribution License 1.0 @@ -792,34 +877,59 @@ Common Public License 1.0 - + - Apache License 1.0 + Code Project Open License 1.02 - + - SIL Open Font License 1.0 + Crossword License - + - Creative Commons Attribution 4.0 International + CrystalStacker License - + - DSDP License + CUA Office Public License v1.0 - + - IBM PowerPC Initialization and Boot Software + Cube License - + - MIT No Attribution + curl License + + + + + Deutsche Freie Software Lizenz + + + + + diffmark license + + + + + Data licence Germany – attribution – version 2.0 + + + + + DOC License + + + + + Dotseqn License @@ -827,964 +937,1299 @@ Detection Rule License 1.0 - + - zlib License + DSDP License - + - Adaptive Public License 1.0 + David M. Gay dtoa License - + - Sybase Open Watcom Public License 1.0 + dvipdfm License - + - GNU General Public License v2.0 w/GCC Runtime Library exception + Educational Community License v1.0 - + - European Union Public License 1.2 + Educational Community License v2.0 - + + + eCos license version 2.0 + + + + + Eiffel Forum License v1.0 + + + + + Eiffel Forum License v2.0 + + + + + eGenix.com Public License 1.1.0 + + + + + Elastic License 2.0 + + + + + Entessa Public License v1.0 + + + + + EPICS Open License + + + + + Eclipse Public License 1.0 + + + + + Eclipse Public License 2.0 + + + + + Erlang Public License v1.1 + + + + + Etalab Open License 2.0 + + + + + EU DataGrid Software License + + + + + European Union Public License 1.0 + + + + + European Union Public License 1.1 + + + + + European Union Public License 1.2 + + + + + Eurosym License + + + + + Fair License + + + + + Fraunhofer FDK AAC Codec Library + + + + + Frameworx Open License 1.0 + + + + + FreeBSD Documentation License + + + + + FreeImage Public License v1.0 + + + + + FSF All Permissive License + + + + + FSF Unlimited License + + + + + FSF Unlimited License (with License Retention) + + + + + FSF Unlimited License (With License Retention and Warranty Disclaimer) + + + + + Freetype Project License + + + + + GD License + + + + + GNU Free Documentation License v1.1 + + + + + GNU Free Documentation License v1.1 only - invariants + + + + + GNU Free Documentation License v1.1 or later - invariants + + + + + GNU Free Documentation License v1.1 only - no invariants + + + + + GNU Free Documentation License v1.1 or later - no invariants + + + + + GNU Free Documentation License v1.1 only + + + + + GNU Free Documentation License v1.1 or later + + + + + GNU Free Documentation License v1.2 + + + + + GNU Free Documentation License v1.2 only - invariants + + + + + GNU Free Documentation License v1.2 or later - invariants + + + + + GNU Free Documentation License v1.2 only - no invariants + + + + + GNU Free Documentation License v1.2 or later - no invariants + + + + + GNU Free Documentation License v1.2 only + + + + + GNU Free Documentation License v1.2 or later + + + + + GNU Free Documentation License v1.3 + + + + + GNU Free Documentation License v1.3 only - invariants + + + + + GNU Free Documentation License v1.3 or later - invariants + + + + + GNU Free Documentation License v1.3 only - no invariants + + + + + GNU Free Documentation License v1.3 or later - no invariants + + + + + GNU Free Documentation License v1.3 only + + + + + GNU Free Documentation License v1.3 or later + + + + + Giftware License + + + + + GL2PS License + + + + + 3dfx Glide License + + + + + Glulxe License + + + + + Good Luck With That Public License + + + + + gnuplot License + + + + + GNU General Public License v1.0 only + + + + + GNU General Public License v1.0 or later + + + + + GNU General Public License v1.0 only + + + + + GNU General Public License v1.0 or later + + + + + GNU General Public License v2.0 only + + + + + GNU General Public License v2.0 or later + + + + + GNU General Public License v2.0 only + + + + + GNU General Public License v2.0 or later + + + + + GNU General Public License v2.0 w/Autoconf exception + + + + + GNU General Public License v2.0 w/Bison exception + + + + + GNU General Public License v2.0 w/Classpath exception + + + + + GNU General Public License v2.0 w/Font exception + + + + + GNU General Public License v2.0 w/GCC Runtime Library exception + + + - FSF Unlimited License + GNU General Public License v3.0 only - + - NASA Open Source Agreement 1.3 + GNU General Public License v3.0 or later - + - BSD 2-Clause "Simplified" License + GNU General Public License v3.0 only - + - XFree86 License 1.1 + GNU General Public License v3.0 or later - + - Eurosym License + GNU General Public License v3.0 w/Autoconf exception - + - Open LDAP Public License v2.8 + GNU General Public License v3.0 w/GCC Runtime Library exception - + - dvipdfm License + Graphics Gems License - + - NIST Public Domain Notice + gSOAP Public License v1.3b - + - Apache License 1.1 + Haskell Language Report License - + - The Parity Public License 6.0.0 + Hippocratic License 2.1 - + - Creative Commons Attribution 2.0 Generic + Hewlett-Packard 1986 License - + - GNU Lesser General Public License v3.0 or later + Historical Permission Notice and Disclaimer - + - BSD 2-Clause with views sentence + HPND with US Government export control warning - + - GNU General Public License v2.0 w/Classpath exception + Historical Permission Notice and Disclaimer - Markus Kuhn variant - + - BSD 3-Clause No Nuclear Warranty + Historical Permission Notice and Disclaimer - sell variant - + - X11 License + HPND sell variant with MIT disclaimer - + - Community Data License Agreement Permissive 2.0 + HTML Tidy License - + - Haskell Language Report License + IBM PowerPC Initialization and Boot Software - + - Artistic License 1.0 w/clause 8 + ICU License - + - Apple Public Source License 2.0 + IEC Code Components End-user licence agreement - + - GNU General Public License v3.0 or later + Independent JPEG Group License - + - Solderpad Hardware License v0.5 + Independent JPEG Group License - short - + - CNRI Python Open Source GPL Compatible License Agreement + ImageMagick License - + - Condor Public License v1.1 + iMatix Standard Function Library Agreement - + - Open LDAP Public License v2.3 + Imlib2 License - + - GNU General Public License v2.0 only + Info-ZIP License - + - Business Source License 1.1 + Inner Net License v2.0 - + - Licence Libre du Québec – Réciprocité version 1.1 + Intel Open Source License - + - Academy of Motion Picture Arts and Sciences BSD + Intel ACPI Software License Agreement - + - copyleft-next 0.3.1 + Interbase Public License v1.0 - + - GNU Free Documentation License v1.3 or later - invariants + IPA Font License - + - Open LDAP Public License v2.7 + IBM Public License v1.0 - + - Open Software License 2.0 + ISC License - + - Unicode License Agreement - Data Files and Software (2015) + Jam License - + - Computer Associates Trusted Open Source License 1.1 + JasPer License - + - Ricoh Source Code Public License + JPL Image Use Policy - + - PNG Reference Library version 2 + Japan Network Information Center License - + - LaTeX Project Public License v1.1 + JSON License - + - Community Data License Agreement Sharing 1.0 + Kazlib License - + - Glulxe License + Knuth CTAN License - + - GNU Free Documentation License v1.3 or later - no invariants + Licence Art Libre 1.2 - + - Open LDAP Public License v1.2 + Licence Art Libre 1.3 - + - Common Development and Distribution License 1.1 + Latex2e License - + - CERN Open Hardware Licence v1.1 + Latex2e with translated notice permission - + - BSD Source Code Attribution + Leptonica License - + - Independent JPEG Group License + GNU Library General Public License v2 only - + - Zimbra Public License v1.4 + GNU Library General Public License v2 or later - + - BSD Zero Clause License + GNU Library General Public License v2 only - + - Creative Commons Attribution 1.0 Generic + GNU Library General Public License v2 or later - + - wxWindows Library License + GNU Lesser General Public License v2.1 only - + - Zope Public License 2.1 + GNU Lesser General Public License v2.1 or later - + - NTP License + GNU Lesser General Public License v2.1 only - + - Artistic License 1.0 (Perl) + GNU Lesser General Public License v2.1 or later - + - Creative Commons Attribution No Derivatives 2.0 Generic + GNU Lesser General Public License v3.0 only - + - Creative Commons Attribution No Derivatives 4.0 International + GNU Lesser General Public License v3.0 or later - + - Adobe Systems Incorporated Source Code License Agreement + GNU Lesser General Public License v3.0 only - + - Eclipse Public License 1.0 + GNU Lesser General Public License v3.0 or later - + - diffmark license + Lesser General Public License For Linguistic Resources - + - xinetd License + libpng License - + - Plexus Classworlds License + PNG Reference Library version 2 - + - Japan Network Information Center License + libselinux public domain notice - + - Adobe Glyph List License + libtiff License - + - Cube License + libutil David Nugent License - + - TCP Wrappers License + Licence Libre du Québec – Permissive version 1.1 - + - Creative Commons Attribution Share Alike 1.0 Generic + Licence Libre du Québec – Réciprocité version 1.1 - + - BSD 2-Clause FreeBSD License + Licence Libre du Québec – Réciprocité forte version 1.1 - + - Open Government Licence - Canada + Linux man-pages - 1 paragraph - + - ANTLR Software Rights Notice + Linux man-pages Copyleft - + - GNU Library General Public License v2.1 or later + Linux man-pages Copyleft - 2 paragraphs - + - Open Software License 2.1 + Linux man-pages Copyleft Variant - + - psutils License + Linux Kernel Variant of OpenIB.org license - + - SCEA Shared Source License + Common Lisp LOOP License - + - The MirOS Licence + Lucent Public License Version 1.0 - + - Hippocratic License 2.1 + Lucent Public License v1.02 - + - GNU Free Documentation License v1.2 only - invariants + LaTeX Project Public License v1.0 - + - GNU Lesser General Public License v2.1 only + LaTeX Project Public License v1.1 - + - Entessa Public License v1.0 + LaTeX Project Public License v1.2 - + - Microsoft Reciprocal License + LaTeX Project Public License v1.3a - + - libselinux public domain notice + LaTeX Project Public License v1.3c - + - GNU Library General Public License v2 only + LZMA SDK License (versions 9.11 to 9.20) - + - Open LDAP Public License v2.5 + LZMA SDK License (versions 9.22 and beyond) - + - Imlib2 License + MakeIndex License - + - libpng License + Martin Birgmeier License - + - Scheme Language Report License + metamail License - + - Mozilla Public License 1.0 + Minpack License - + - Sax Public Domain Notice + The MirOS Licence - + - Norwegian Licence for Open Government Data (NLOD) 1.0 + MIT License - + - Simple Public License 2.0 + MIT No Attribution - + - Technische Universitaet Berlin License 1.0 + Enlightenment License (e16) - + - GNU Free Documentation License v1.1 only - no invariants + CMU License - + - Creative Commons Attribution No Derivatives 3.0 Germany + enna License - + - MakeIndex License + feh License - + - EPICS Open License + MIT Festival Variant - + - GNU Free Documentation License v1.3 only - invariants + MIT License Modern Variant - + - XSkat License + MIT Open Group variant - + - bzip2 and libbzip2 License v1.0.5 + MIT Tom Wu Variant - + - Community Specification License 1.0 + MIT +no-false-attribs license - + - GL2PS License + Motosoto License - + - Historical Permission Notice and Disclaimer + mpi Permissive License - + - bzip2 and libbzip2 License v1.0.6 + mpich2 License - + - Creative Commons Attribution Non Commercial 1.0 Generic + Mozilla Public License 1.0 - + - Fair License + Mozilla Public License 1.1 - + - CeCILL-B Free Software License Agreement + Mozilla Public License 2.0 - + - 3dfx Glide License + Mozilla Public License 2.0 (no copyleft exception) - + - Creative Commons Attribution Share Alike 4.0 International + mplus Font License - + - Creative Commons Zero v1.0 Universal + Microsoft Limited Public License - + - enna License + Microsoft Public License - + - Wsuipa License + Microsoft Reciprocal License - + - RSA Message-Digest License + Matrix Template Library License - + - VOSTROM Public License for Open Source + Mulan Permissive Software License, Version 1 - + - Open Use of Data Agreement v1.0 + Mulan Permissive Software License, Version 2 - + - CERN Open Hardware Licence Version 2 - Strongly Reciprocal + Multics License - + - X11 License Distribution Modification Variant + Mup License - + - copyleft-next 0.3.0 + Nara Institute of Science and Technology License (2003) - + - Zimbra Public License v1.3 + NASA Open Source Agreement 1.3 - + - NIST Public Domain Notice with license fallback + Naumen Public License - + - Nokia Open Source License + Net Boolean Public License v1 - + - Academic Free License v2.1 + Non-Commercial Government Licence - + - Zope Public License 2.0 + University of Illinois/NCSA Open Source License - + - Open Data Commons Open Database License v1.0 + Net-SNMP License - + - zlib/libpng License with Acknowledgement + NetCDF license - + - PHP License v3.01 + Newsletr License - + - Afmparse License + Nethack General Public License - + - Historical Permission Notice and Disclaimer - sell variant + NICTA Public Software License, Version 1.0 - + - PolyForm Small Business License 1.0.0 + NIST Public Domain Notice - + - IBM Public License v1.0 + NIST Public Domain Notice with license fallback - + - CeCILL Free Software License Agreement v1.1 + NIST Software License - + - feh License + Norwegian Licence for Open Government Data (NLOD) 1.0 - + - SIL Open Font License 1.0 with Reserved Font Name + Norwegian Licence for Open Government Data (NLOD) 2.0 - + - TMate Open Source License + No Limit Public License - + - BSD 3-Clause No Nuclear License + Nokia Open Source License - + - W3C Software Notice and License (1998-07-20) + Netizen Open Source License - + - Sun Public License v1.0 + Noweb License - + - NetCDF license + Netscape Public License v1.0 - + - Aladdin Free Public License + Netscape Public License v1.1 - + - AMD's plpa_map.c License + Non-Profit Open Software License 3.0 - + - CrystalStacker License + NRL License - + - Intel ACPI Software License Agreement + NTP License - + - CERN Open Hardware Licence v1.2 + NTP No Attribution - + - Creative Commons Attribution Non Commercial Share Alike 3.0 Germany + Nunit License - + - MIT License + Open Use of Data Agreement v1.0 - + - Zed License + Open CASCADE Technology Public License - + - Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B) + OCLC Research Public License 2.0 - + - Mulan Permissive Software License, Version 1 + Open Data Commons Open Database License v1.0 - + - Eiffel Forum License v2.0 + Open Data Commons Attribution License v1.0 - + - Latex2e License + OFFIS License - + - Spencer License 94 + SIL Open Font License 1.0 - + - Open Public License v1.0 + SIL Open Font License 1.0 with no Reserved Font Name - + - Creative Commons Attribution Non Commercial 4.0 International + SIL Open Font License 1.0 with Reserved Font Name - + - GNU Lesser General Public License v3.0 or later + SIL Open Font License 1.1 - + - Universal Permissive License v1.0 + SIL Open Font License 1.1 with no Reserved Font Name - + - University of Illinois/NCSA Open Source License + SIL Open Font License 1.1 with Reserved Font Name - + - SGI Free Software License B v2.0 + OGC Software License, Version 1.0 - + - GNU General Public License v3.0 w/GCC Runtime Library exception + Taiwan Open Government Data License, version 1.0 - + - Zend License v2.0 + Open Government Licence - Canada - + - ImageMagick License + Open Government Licence v1.0 - + - Open LDAP Public License v2.6 + Open Government Licence v2.0 - + - Unicode Terms of Use + Open Government Licence v3.0 - + - GNU General Public License v3.0 only + Open Group Test Suite License - + - Artistic License 2.0 + Open LDAP Public License v1.1 - + - SQLite Blessing + Open LDAP Public License v1.2 - + - Etalab Open License 2.0 + Open LDAP Public License v1.3 - + - GNU Free Documentation License v1.2 only + Open LDAP Public License v1.4 - + - LaTeX Project Public License v1.0 + Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B) - + - Rdisc License + Open LDAP Public License v2.0.1 - + - BSD 3-Clause Modification + Open LDAP Public License v2.1 - + - Xerox License + Open LDAP Public License v2.2 - + - Mozilla Public License 2.0 + Open LDAP Public License v2.2.1 - + - BitTorrent Open Source License v1.1 + Open LDAP Public License 2.2.2 - + - Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic + Open LDAP Public License v2.3 - + - Sun Industry Standards Source License v1.1 + Open LDAP Public License v2.4 - + - libtiff License + Open LDAP Public License v2.5 - + - Creative Commons Attribution Non Commercial Share Alike 2.0 England and Wales + Open LDAP Public License v2.6 - + - Deutsche Freie Software Lizenz + Open LDAP Public License v2.7 - + - LaTeX Project Public License v1.2 + Open LDAP Public License v2.8 - + - TAPR Open Hardware License v1.0 + Open Logistics Foundation License Version 1.3 - + - European Union Public License 1.0 + Open Market License - + - Solderpad Hardware License, Version 0.51 + OpenPBS v2.3 Software License - + - Freetype Project License + OpenSSL License - + - W3C Software Notice and Document License (2015-05-13) + Open Public License v1.0 - + - OSET Public License version 2.1 + United Kingdom Open Parliament Licence v3.0 - + - EU DataGrid Software License + Open Publication License v1.0 - + - Upstream Compatibility License v1.0 + OSET Public License version 2.1 - + - Borceux license + Open Software License 1.0 - + - Elastic License 2.0 + Open Software License 1.1 - + - BSD 2-Clause NetBSD License + Open Software License 2.0 - + - BSD 3-Clause Open MPI variant + Open Software License 2.1 @@ -1792,655 +2237,660 @@ Open Software License 3.0 - + - curl License + The Parity Public License 6.0.0 - + - Spencer License 86 + The Parity Public License 7.0.0 - + - Boost Software License 1.0 + Open Data Commons Public Domain Dedication & License 1.0 - + - Standard ML of New Jersey License + PHP License v3.0 - + - Trusster Open Source License + PHP License v3.01 - + - Netizen Open Source License + Plexus Classworlds License - + - Academic Free License v1.2 + PolyForm Noncommercial License 1.0.0 - + - Mulan Permissive Software License, Version 2 + PolyForm Small Business License 1.0.0 - + - Motosoto License + PostgreSQL License - + - Creative Commons Attribution Non Commercial Share Alike 2.5 Generic + Python Software Foundation License 2.0 - + - JasPer License + psfrag License - + - BSD-4-Clause (University of California-Specific) + psutils License - + - Bahyph License + Python License 2.0 - + - Vovida Software License v1.0 + Python License 2.0.1 - + - W3C Software Notice and License (2002-12-31) + Qhull License - + - Open Data Commons Attribution License v1.0 + Q Public License 1.0 - + - BitTorrent Open Source License v1.0 + Q Public License 1.0 - INRIA 2004 variant - + - Open Government Licence v2.0 + Rdisc License - + - GNU Lesser General Public License v3.0 only + Red Hat eCos Public License v1.1 - + - X.Net License + Reciprocal Public License 1.1 - + - Ruby License + Reciprocal Public License 1.5 - + - GNU Free Documentation License v1.3 + RealNetworks Public Source License v1.0 - + - Zope Public License 1.1 + RSA Message-Digest License - + - Open CASCADE Technology Public License + Ricoh Source Code Public License - + - LaTeX Project Public License v1.3c + Ruby License - + - Apache License 2.0 + Sax Public Domain Notice - + - GD License + Saxpath License - + - Creative Commons Attribution 3.0 Netherlands + SCEA Shared Source License - + - LaTeX Project Public License v1.3a + Scheme Language Report License - + - Creative Commons Attribution 2.5 Australia + Sendmail License - + - GNU Free Documentation License v1.1 only + Sendmail License 8.23 - + - GNU Free Documentation License v1.1 or later + SGI Free Software License B v1.0 - + - Open Government Licence v3.0 + SGI Free Software License B v1.1 - + - Yahoo! Public License v1.0 + SGI Free Software License B v2.0 - + - Reciprocal Public License 1.1 + SGP4 Permission Notice - + - GNU Library General Public License v2 or later + Solderpad Hardware License v0.5 - + - Open Publication License v1.0 + Solderpad Hardware License, Version 0.51 - + - Noweb License + Simple Public License 2.0 - + - Academic Free License v3.0 + Sun Industry Standards Source License v1.1 - + - Nunit License + Sun Industry Standards Source License v1.2 - + - Creative Commons Attribution 3.0 Unported + Sleepycat License - + - Beerware License + Standard ML of New Jersey License - + - Caldera License + Secure Messaging Protocol Public License - + - GNU General Public License v1.0 only + SNIA Public License 1.1 - + - GNU General Public License v2.0 or later + snprintf License - + - Non-Commercial Government Licence + Spencer License 86 - + - Creative Commons Attribution No Derivatives 2.5 Generic + Spencer License 94 - + - GNU General Public License v2.0 only + Spencer License 99 - + - Intel Open Source License + Sun Public License v1.0 - + - Vim License + SSH OpenSSH license - + - Creative Commons Attribution Share Alike 2.0 Generic + SSH short notice - + - MIT +no-false-attribs license + Server Side Public License, v 1 - + - Apple Public Source License 1.1 + Standard ML of New Jersey License - + - GNU Free Documentation License v1.2 or later + SugarCRM Public License v1.1.3 - + - BSD with attribution + SunPro License - + - SIL Open Font License 1.0 with no Reserved Font Name + Scheme Widget Library (SWL) Software License Agreement - + - Naumen Public License + Symlinks License - + - Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic + TAPR Open Hardware License v1.0 - + - Computational Use of Data Agreement v1.0 + TCL/TK License - + - Lesser General Public License For Linguistic Resources + TCP Wrappers License - + - mpich2 License + TermReadKey License - + - Apple Public Source License 1.0 + TMate Open Source License - + - Linux Kernel Variant of OpenIB.org license + TORQUE v2.5+ Software License v1.1 - + - Enlightenment License (e16) + Trusster Open Source License - + - GNU Free Documentation License v1.2 + Time::ParseDate License - + - Open Group Test Suite License + THOR Public License 1.0 - + - Dotseqn License + Text-Tabs+Wrap License - + - Data licence Germany – attribution – version 2.0 + Technische Universitaet Berlin License 1.0 - + - Saxpath License + Technische Universitaet Berlin License 2.0 - + - GNU Affero General Public License v3.0 + UCAR License - + - Abstyles License + Upstream Compatibility License v1.0 - + - Creative Commons Attribution Share Alike 3.0 Unported + Unicode License Agreement - Data Files and Software (2015) - + - Giftware License + Unicode License Agreement - Data Files and Software (2016) - + - FreeImage Public License v1.0 + Unicode Terms of Use - + - CeCILL Free Software License Agreement v2.1 + UnixCrypt License - + - RealNetworks Public Source License v1.0 + The Unlicense - + - GNU Free Documentation License v1.3 or later + Universal Permissive License v1.0 - + - GNU Free Documentation License v1.1 or later - invariants + Vim License - + - Educational Community License v2.0 + VOSTROM Public License for Open Source - + - Licence Libre du Québec – Réciprocité forte version 1.1 + Vovida Software License v1.0 - + - GNU General Public License v3.0 w/Autoconf exception + W3C Software Notice and License (2002-12-31) - + - Jam License + W3C Software Notice and License (1998-07-20) - + - GNU Free Documentation License v1.2 or later - no invariants + W3C Software Notice and Document License (2015-05-13) - + - CeCILL Free Software License Agreement v2.0 + w3m License - + - PolyForm Noncommercial License 1.0.0 + Sybase Open Watcom Public License 1.0 - + - OGC Software License, Version 1.0 + Widget Workshop License - + - Creative Commons Attribution No Derivatives 3.0 Unported + Wsuipa License - + - Q Public License 1.0 + Do What The F*ck You Want To Public License - + - Licence Art Libre 1.2 + wxWindows Library License - + - Creative Commons Attribution 3.0 Germany + X11 License - + - OpenSSL License + X11 License Distribution Modification Variant - + - Spencer License 99 + Xdebug License v 1.03 - + - Creative Commons Attribution Share Alike 3.0 Austria + Xerox License - + - BSD Protection License + Xfig License - + - Open LDAP Public License 2.2.2 + XFree86 License 1.1 - + - NRL License + xinetd License - + - TORQUE v2.5+ Software License v1.1 + xlock License - + - HTML Tidy License + X.Net License - + - Server Side Public License, v 1 + XPP License - + - Netscape Public License v1.0 + XSkat License - + - GNU Library General Public License v2 only + Yahoo! Public License v1.0 - + - GNU Affero General Public License v3.0 only + Yahoo! Public License v1.1 - + - GNU Free Documentation License v1.2 or later - invariants + Zed License - + - GNU General Public License v2.0 w/Bison exception + Zend License v2.0 - + - Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic + Zimbra Public License v1.3 - + - Educational Community License v1.0 + Zimbra Public License v1.4 - + - Do What The F*ck You Want To Public License + zlib License - + - Creative Commons Attribution Share Alike 2.0 England and Wales + zlib/libpng License with Acknowledgement - + - GNU General Public License v3.0 only + Zope Public License 1.1 - + - Open LDAP Public License v2.2.1 + Zope Public License 2.0 - + - Secure Messaging Protocol Public License + Zope Public License 2.1 - + + - Creative Commons Attribution 3.0 Austria + 389 Directory Server Exception - + - Eiffel Forum License v1.0 + Asterisk exception - + - Net Boolean Public License v1 + Autoconf exception 2.0 - + - Lawrence Berkeley National Labs BSD variant license + Autoconf exception 3.0 - + - Affero General Public License v1.0 + Autoconf generic exception - + - Crossword License + Autoconf macro exception - + - TCL/TK License + Bison exception 2.2 - + - Creative Commons Attribution No Derivatives 1.0 Generic + Bootloader Distribution Exception - + - Apple MIT License + Classpath exception 2.0 - + - Technische Universitaet Berlin License 2.0 + CLISP exception 2.0 - + - GNU Free Documentation License v1.3 only + cryptsetup OpenSSL exception - + - Non-Profit Open Software License 3.0 + DigiRule FOSS License Exception - + - BSD 4-Clause "Original" or "Old" License + eCos exception 2.0 - + - gSOAP Public License v1.3b + Fawkes Runtime Exception - + - GNU Lesser General Public License v2.1 only + FLTK exception - + - GNU Lesser General Public License v3.0 only + Font exception 2.0 - FreeRTOS Exception 2.0 - + - Swift Exception + GCC Runtime Library exception 2.0 - + - Qt LGPL exception 1.1 + GCC Runtime Library exception 3.1 + + + + + GNAT exception @@ -2448,69 +2898,69 @@ GNU JavaMail exception - + - CLISP exception 2.0 + GPL-3.0 Interface Exception - + - eCos exception 2.0 + GPL-3.0 Linking Exception - + - GPL Cooperation Commitment 1.0 + GPL-3.0 Linking Exception (with Corresponding Source) - + - DigiRule FOSS License Exception + GPL Cooperation Commitment 1.0 - + - Font exception 2.0 + GStreamer Exception (2005) - + - Qt GPL exception 1.0 + GStreamer Exception (2008) - + - PS/PDF font exception (2017-08-17) + i2p GPL+Java Exception - + - GPL-3.0 Linking Exception (with Corresponding Source) + KiCad Libraries Exception - + - Linux Syscall Note + LGPL-3.0 Linking Exception - + - GCC Runtime Library exception 2.0 + libpri OpenH323 exception - + - LZMA exception + Libtool Exception - + - Autoconf exception 3.0 + Linux Syscall Note - + - U-Boot exception 2.0 + LLGPL Preamble @@ -2518,69 +2968,59 @@ LLVM Exception - - - OCaml LGPL Linking Exception - - - - - Autoconf exception 2.0 - - - + - Bootloader Distribution Exception + LZMA exception - + - LGPL-3.0 Linking Exception + Macros and Inline Functions Exception - + - OpenVPN OpenSSL Exception + Nokia Qt LGPL exception 1.1 - + - FLTK exception + OCaml LGPL Linking Exception - + - Bison exception 2.2 + Open CASCADE Exception 1.0 - + - Open CASCADE Exception 1.0 + OpenJDK Assembly exception 1.0 - + - GCC Runtime Library exception 3.1 + OpenVPN OpenSSL Exception - + - OpenJDK Assembly exception 1.0 + PS/PDF font exception (2017-08-17) - + - WxWindows Library Exception 3.1 + INRIA QPL 1.0 2004 variant exception - + - Fawkes Runtime Exception + Qt GPL exception 1.0 - + - Nokia Qt LGPL exception 1.1 + Qt LGPL exception 1.1 @@ -2588,49 +3028,49 @@ Qwt exception 1.0 - + - Universal FOSS Exception, Version 1.0 + Solderpad Hardware License v2.0 - + - Classpath exception 2.0 + Solderpad Hardware License v2.1 - + - Solderpad Hardware License v2.0 + SWI exception - + - GPL-3.0 Linking Exception + Swift Exception - + - Solderpad Hardware License v2.1 + U-Boot exception 2.0 - + - Libtool Exception + Universal FOSS Exception, Version 1.0 - + - Macros and Inline Functions Exception + vsftpd OpenSSL exception - + - 389 Directory Server Exception + WxWindows Library Exception 3.1 - + - i2p GPL+Java Exception + x11vnc OpenSSL Exception diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 00000000000..a9f521af0a7 --- /dev/null +++ b/templates/README.md @@ -0,0 +1,23 @@ +# Grype Templates + +This folder contains a set of "helper" go templates you can use for your own reports. + +Please feel free to extend and/or update the templates for your needs, be sure to contribute back into this folder any new templates! + +Current templates: + +
+.
+├── README.md
+└── table.tmpl
+
+ +## Table + +This template mimics the "default" table output of Grype, there are some drawbacks using the template vs the native output such as: + +- unsorted +- duplicate rows +- no (wont-fix) logic + +As you can see from the above list, it's not perfect but it's a start. diff --git a/templates/csv.tmpl b/templates/csv.tmpl new file mode 100644 index 00000000000..738185ffc42 --- /dev/null +++ b/templates/csv.tmpl @@ -0,0 +1,4 @@ +"Package","Version Installed","Vulnerability ID","Severity" +{{- range .Matches}} +"{{.Artifact.Name}}","{{.Artifact.Version}}","{{.Vulnerability.ID}}","{{.Vulnerability.Severity}}" +{{- end}} diff --git a/templates/junit.tmpl b/templates/junit.tmpl new file mode 100644 index 00000000000..01bd63aff6c --- /dev/null +++ b/templates/junit.tmpl @@ -0,0 +1,14 @@ + + +{{- $failures := len $.Matches }} + + + + + {{- range .Matches }} + + {{ .Vulnerability.Description }} {{ .Artifact.CPEs }} {{ .Vulnerability.DataSource }} + + {{- end }} + + \ No newline at end of file diff --git a/templates/table.tmpl b/templates/table.tmpl new file mode 100644 index 00000000000..519cfef03d8 --- /dev/null +++ b/templates/table.tmpl @@ -0,0 +1,48 @@ +{{- $name_length := 4}} +{{- $installed_length := 9}} +{{- $fixed_in_length := 8}} +{{- $type_length := 4}} +{{- $vulnerability_length := 13}} +{{- $severity_length := 8}} +{{- range .Matches}} +{{- $temp_name_length := (len .Artifact.Name)}} +{{- $temp_installed_length := (len .Artifact.Version)}} +{{- $temp_fixed_in_length := (len (.Vulnerability.Fix.Versions | join " "))}} +{{- $temp_type_length := (len .Artifact.Type)}} +{{- $temp_vulnerability_length := (len .Vulnerability.ID)}} +{{- $temp_severity_length := (len .Vulnerability.Severity)}} +{{- if (lt $name_length $temp_name_length) }} +{{- $name_length = $temp_name_length}} +{{- end}} +{{- if (lt $installed_length $temp_installed_length) }} +{{- $installed_length = $temp_installed_length}} +{{- end}} +{{- if (lt $fixed_in_length $temp_fixed_in_length) }} +{{- $fixed_in_length = $temp_fixed_in_length}} +{{- end}} +{{- if (lt $type_length $temp_type_length) }} +{{- $type_length = $temp_type_length}} +{{- end}} +{{- if (lt $vulnerability_length $temp_vulnerability_length) }} +{{- $vulnerability_length = $temp_vulnerability_length}} +{{- end}} +{{- if (lt $severity_length $temp_severity_length) }} +{{- $severity_length = $temp_severity_length}} +{{- end}} +{{- end}} +{{- $name_length = add $name_length 2}} +{{- $pad_name := repeat (int $name_length) " "}} +{{- $installed_length = add $installed_length 2}} +{{- $pad_installed := repeat (int $installed_length) " "}} +{{- $fixed_in_length = add $fixed_in_length 2}} +{{- $pad_fixed_in := repeat (int $fixed_in_length) " "}} +{{- $type_length = add $type_length 2}} +{{- $pad_type := repeat (int $type_length) " "}} +{{- $vulnerability_length = add $vulnerability_length 2}} +{{- $pad_vulnerability := repeat (int $vulnerability_length) " "}} +{{- $severity_length = add $severity_length 2}} +{{- $pad_severity := repeat (int $severity_length) " "}} +{{cat "NAME" (substr 5 (int $name_length) $pad_name)}}{{cat "INSTALLED" (substr 10 (int $installed_length) $pad_installed)}}{{cat "FIXED-IN" (substr 9 (int $fixed_in_length) $pad_fixed_in)}}{{cat "TYPE" (substr 5 (int $type_length) $pad_type)}}{{cat "VULNERABILITY" (substr 14 (int $vulnerability_length) $pad_vulnerability)}}{{cat "SEVERITY" (substr 9 (int $severity_length) $pad_severity)}} +{{- range .Matches}} +{{cat .Artifact.Name (substr (int (add (len .Artifact.Name) 1)) (int $name_length) $pad_name)}}{{cat .Artifact.Version (substr (int (add (len .Artifact.Version) 1)) (int $installed_length) $pad_installed)}}{{cat (.Vulnerability.Fix.Versions | join " ") (substr (int (add (len (.Vulnerability.Fix.Versions | join " ")) 1)) (int $fixed_in_length) $pad_fixed_in)}}{{cat .Artifact.Type (substr (int (add (len .Artifact.Type) 1)) (int $type_length) $pad_type)}}{{cat .Vulnerability.ID (substr (int (add (len .Vulnerability.ID) 1)) (int $vulnerability_length) $pad_vulnerability)}}{{cat .Vulnerability.Severity (substr (int (add (len .Vulnerability.Severity) 1)) (int $severity_length) $pad_severity)}} +{{- end}} \ No newline at end of file diff --git a/test/cli/cmd_test.go b/test/cli/cmd_test.go index ad9f79303a0..692d144fc57 100644 --- a/test/cli/cmd_test.go +++ b/test/cli/cmd_test.go @@ -1,8 +1,11 @@ package cli import ( + "encoding/json" "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestCmd(t *testing.T) { @@ -39,7 +42,7 @@ func TestCmd(t *testing.T) { }, { name: "responds-to-search-options", - args: []string{"-vv"}, + args: []string{"--help"}, env: map[string]string{ "GRYPE_SEARCH_UNINDEXED_ARCHIVES": "true", "GRYPE_SEARCH_INDEXED_ARCHIVES": "false", @@ -51,7 +54,41 @@ func TestCmd(t *testing.T) { // package-cataloger-level options. assertInOutput("unindexed-archives: true"), assertInOutput("indexed-archives: false"), - assertInOutput("scope: all-layers"), + 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: "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)"}), }, }, } @@ -70,3 +107,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/registry_auth_test.go b/test/cli/registry_auth_test.go index a06c28a041c..f1e1e66a7d5 100644 --- a/test/cli/registry_auth_test.go +++ b/test/cli/registry_auth_test.go @@ -18,7 +18,7 @@ func TestRegistryAuth(t *testing.T) { assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), 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`), }, }, { @@ -40,7 +40,7 @@ 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"), @@ -57,7 +57,7 @@ func TestRegistryAuth(t *testing.T) { assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), 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/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/trait_assertions_test.go b/test/cli/trait_assertions_test.go index fa89d35ea08..9681c91e154 100644 --- a/test/cli/trait_assertions_test.go +++ b/test/cli/trait_assertions_test.go @@ -32,3 +32,34 @@ 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) + } + } +} 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/environments/Dockerfile-alpine-3.6 b/test/install/environments/Dockerfile-alpine-3.6 index 982e5402996..f0a5fcf7293 100644 --- a/test/install/environments/Dockerfile-alpine-3.6 +++ b/test/install/environments/Dockerfile-alpine-3.6 @@ -1,2 +1,2 @@ -FROM alpine:3.6 -RUN apk update && apk add python3 wget unzip make ca-certificates \ No newline at end of file +FROM alpine:3.6@sha256:66790a2b79e1ea3e1dabac43990c54aca5d1ddf268d9a5a0285e4167c8b24475 +RUN apk update && apk add python3 wget unzip make ca-certificates diff --git a/test/install/environments/Dockerfile-ubuntu-20.04 b/test/install/environments/Dockerfile-ubuntu-20.04 index dafb64ed73d..07341fc3830 100644 --- a/test/install/environments/Dockerfile-ubuntu-20.04 +++ b/test/install/environments/Dockerfile-ubuntu-20.04 @@ -1,2 +1,2 @@ -FROM ubuntu:20.04 -RUN apt update -y && apt install make python3 curl unzip -y \ No newline at end of file +FROM ubuntu:20.04@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba +RUN apt update -y && apt install make python3 curl unzip -y diff --git a/test/integration/compare_sbom_input_vs_lib_test.go b/test/integration/compare_sbom_input_vs_lib_test.go index 0caed5ac565..6040d1c657c 100644 --- a/test/integration/compare_sbom_input_vs_lib_test.go +++ b/test/integration/compare_sbom_input_vs_lib_test.go @@ -36,12 +36,6 @@ func getListingURL() string { } func TestCompareSBOMInputToLibResults(t *testing.T) { - formats := []sbom.FormatID{ - syft.JSONFormatID, - syft.SPDXJSONFormatID, - syft.SPDXTagValueFormatID, - } - // get a grype DB store, _, closer, err := grype.LoadVulnerabilityDB(db.Config{ DBRootDir: "test-fixtures/grype-db", @@ -60,6 +54,7 @@ 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.RustPkg), string(syftPkg.KbPkg), string(syftPkg.DartPubPkg), @@ -75,53 +70,200 @@ func TestCompareSBOMInputToLibResults(t *testing.T) { string(syftPkg.LinuxKernelPkg), string(syftPkg.LinuxKernelModulePkg), string(syftPkg.Rpkg), + string(syftPkg.SwiftPkg), + string(syftPkg.GithubActionPkg), + string(syftPkg.GithubActionWorkflowPkg), ) observedPkgTypes := strset.New() + testCases := []struct { + name string + image string + format sbom.FormatID + }{ + { + image: "anchore/test_images:vulnerabilities-alpine", + format: syft.JSONFormatID, + name: "alpine-syft-json", + }, + + { + image: "anchore/test_images:vulnerabilities-alpine", + format: syft.SPDXJSONFormatID, + name: "alpine-spdx-json", + }, + + { + image: "anchore/test_images:vulnerabilities-alpine", + format: syft.SPDXTagValueFormatID, + name: "alpine-spdx-tag-value", + }, + + { + image: "anchore/test_images:gems", + format: syft.JSONFormatID, + name: "gems-syft-json", + }, + + { + image: "anchore/test_images:gems", + format: syft.SPDXJSONFormatID, + name: "gems-spdx-json", + }, + + { + image: "anchore/test_images:gems", + format: syft.SPDXTagValueFormatID, + name: "gems-spdx-tag-value", + }, + + { + image: "anchore/test_images:vulnerabilities-debian", + format: syft.JSONFormatID, + name: "debian-syft-json", + }, + + { + image: "anchore/test_images:vulnerabilities-debian", + format: syft.SPDXJSONFormatID, + name: "debian-spdx-json", + }, + + { + image: "anchore/test_images:vulnerabilities-debian", + format: syft.SPDXTagValueFormatID, + name: "debian-spdx-tag-value", + }, + + { + image: "anchore/test_images:vulnerabilities-centos", + format: syft.JSONFormatID, + name: "centos-syft-json", + }, + + { + image: "anchore/test_images:vulnerabilities-centos", + format: syft.SPDXJSONFormatID, + name: "centos-spdx-json", + }, + + { + image: "anchore/test_images:vulnerabilities-centos", + format: syft.SPDXTagValueFormatID, + name: "centos-spdx-tag-value", + }, + + { + image: "anchore/test_images:npm", + format: syft.JSONFormatID, + name: "npm-syft-json", + }, + + { + image: "anchore/test_images:npm", + format: syft.SPDXJSONFormatID, + name: "npm-spdx-json", + }, + + { + image: "anchore/test_images:npm", + format: syft.SPDXTagValueFormatID, + name: "npm-spdx-tag-value", + }, + + { + image: "anchore/test_images:java", + format: syft.JSONFormatID, + name: "java-syft-json", + }, - for _, image := range imagesWithVulnerabilities { - imageArchive := PullThroughImageCache(t, image) + { + image: "anchore/test_images:java", + format: syft.SPDXJSONFormatID, + name: "java-spdx-json", + }, + + { + image: "anchore/test_images:java", + format: syft.SPDXTagValueFormatID, + name: "java-spdx-tag-value", + }, + + { + image: "anchore/test_images:golang-56d52bc", + format: syft.JSONFormatID, + name: "go-syft-json", + }, + + { + image: "anchore/test_images:golang-56d52bc", + format: syft.SPDXJSONFormatID, + name: "go-spdx-json", + }, + + { + image: "anchore/test_images:golang-56d52bc", + format: syft.SPDXTagValueFormatID, + name: "go-spdx-tag-value", + }, + + { + image: "anchore/test_images:arch", + format: syft.JSONFormatID, + name: "arch-syft-json", + }, + + { + image: "anchore/test_images:arch", + format: syft.SPDXJSONFormatID, + name: "arch-spdx-json", + }, + + { + image: "anchore/test_images:arch", + format: syft.SPDXTagValueFormatID, + name: "arch-spdx-tag-value", + }, + } + for _, tc := range testCases { + imageArchive := PullThroughImageCache(t, tc.image) imageSource := fmt.Sprintf("docker-archive:%s", imageArchive) + f := syft.FormatByID(tc.format) + if f == nil { + t.Errorf("Invalid formatID: %s", tc.format) + } + t.Run(tc.name, 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) - for _, formatID := range formats { - f := syft.FormatByID(formatID) - if f == nil { - t.Errorf("Invalid formatID: %s", formatID) + // 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)) } - 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)) - } - }) - } + }) } // 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..6466488872e 100644 --- a/test/integration/db_mock_test.go +++ b/test/integration/db_mock_test.go @@ -100,6 +100,15 @@ func newMockDbStore() *mockStore { }, }, }, + "github:language:idris": { + "my-package": []grypeDB.Vulnerability{ + { + ID: "CVE-bogus-my-package-2-idris", + VersionConstraint: "< 2.0", + VersionFormat: "unknown", + }, + }, + }, "github:language:javascript": { "npm": []grypeDB.Vulnerability{ { @@ -117,13 +126,6 @@ func newMockDbStore() *mockStore { VersionFormat: "python", }, }, - "my-package": []grypeDB.Vulnerability{ - { - ID: "CVE-bogus-my-package-2-python", - VersionConstraint: "< 2.0", - VersionFormat: "python", - }, - }, }, "github:language:ruby": { "bundler": []grypeDB.Vulnerability{ @@ -161,6 +163,22 @@ func newMockDbStore() *mockStore { }, }, }, + "github:language:rust": { + "hello-auditable": []grypeDB.Vulnerability{ + { + ID: "CVE-rust-sample-1", + VersionConstraint: "< 0.2.0", + VersionFormat: "unknown", + }, + }, + "auditable": []grypeDB.Vulnerability{ + { + ID: "CVE-rust-sample-2", + VersionConstraint: "< 0.2.0", + VersionFormat: "unknown", + }, + }, + }, "debian:distro:debian:8": { "apt-dev": []grypeDB.Vulnerability{ { diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 2cf83c32c84..60ef75800de 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/facebookincubator/nvdtools/wfn" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" @@ -15,10 +16,12 @@ import ( "github.com/anchore/grype/grype/matcher" "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" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft" + "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger" "github.com/anchore/syft/syft/source" @@ -107,6 +110,10 @@ func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPk SearchedBy: map[string]interface{}{ "language": "javascript", "namespace": "github:language:javascript", + "package": map[string]string{ + "name": thePkg.Name, + "version": thePkg.Version, + }, }, Found: map[string]interface{}{ "versionConstraint": "> 5, < 7.2.1 (unknown)", @@ -144,6 +151,10 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co SearchedBy: map[string]interface{}{ "language": "python", "namespace": "github:language:python", + "package": map[string]string{ + "name": thePkg.Name, + "version": thePkg.Version, + }, }, Found: map[string]interface{}{ "versionConstraint": "< 2.6.2 (python)", @@ -181,6 +192,10 @@ func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co SearchedBy: map[string]interface{}{ "language": "dotnet", "namespace": "github:language:dotnet", + "package": map[string]string{ + "name": thePkg.Name, + "version": thePkg.Version, + }, }, Found: map[string]interface{}{ "versionConstraint": ">= 3.7.0.0, < 3.7.12.0 (unknown)", @@ -214,6 +229,10 @@ func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll SearchedBy: map[string]interface{}{ "language": "ruby", "namespace": "github:language:ruby", + "package": map[string]string{ + "name": thePkg.Name, + "version": thePkg.Version, + }, }, Found: map[string]interface{}{ "versionConstraint": "> 2.0.0, <= 2.1.4 (unknown)", @@ -233,7 +252,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)") } @@ -248,6 +268,10 @@ 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) @@ -263,6 +287,10 @@ func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Co SearchedBy: map[string]interface{}{ "language": "go", "namespace": "github:language:go", + "package": map[string]string{ + "name": thePkg.Name, + "version": thePkg.Version, + }, }, Found: map[string]interface{}{ "versionConstraint": "< 1.4.0 (unknown)", @@ -306,6 +334,10 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Coll SearchedBy: map[string]interface{}{ "language": "java", "namespace": "github:language:java", + "package": map[string]string{ + "name": thePkg.Name, + "version": thePkg.Version, + }, }, Found: map[string]interface{}{ "versionConstraint": ">= 0.0.1, < 1.2.0 (unknown)", @@ -498,6 +530,10 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C SearchedBy: map[string]any{ "language": "haskell", "namespace": "github:language:haskell", + "package": map[string]string{ + "name": thePkg.Name, + "version": thePkg.Version, + }, }, Found: map[string]any{ "versionConstraint": "< 0.9.0 (unknown)", @@ -509,9 +545,48 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C }) } +func addRustMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, 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) + theVuln := theStore.backend["github:language:rust"][strings.ToLower(thePkg.Name)][0] + vulnObj, err := vulnerability.NewVulnerability(theVuln) + require.NoError(t, err) + + 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 := internal.NewStringSet() - definedMatchers := internal.NewStringSet() + observedMatchers := stringutil.NewStringSet() + definedMatchers := stringutil.NewStringSet() for _, l := range match.AllMatcherTypes { definedMatchers.Add(string(l)) } @@ -567,6 +642,14 @@ func TestMatchByImage(t *testing.T) { return expectedMatches }, }, + { + fixtureImage: "image-rust-auditable-match-coverage", + expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { + expectedMatches := match.NewMatches() + addRustMatches(t, theSource, catalog, theStore, &expectedMatches) + return expectedMatches + }, + }, } for _, test := range tests { @@ -578,13 +661,15 @@ func TestMatchByImage(t *testing.T) { userImage := "docker-archive:" + tarPath - sourceInput, err := source.ParseInput(userImage, "") + detection, err := source.Detect(userImage, source.DetectConfig{}) require.NoError(t, err) // this is purely done to help setup mocks - theSource, cleanup, err := source.New(*sourceInput, nil, nil) + theSource, err := detection.NewSource(source.DetectionSourceConfig{}) require.NoError(t, err) - defer cleanup() + t.Cleanup(func() { + require.NoError(t, theSource.Close()) + }) // TODO: relationships are not verified at this time config := cataloger.DefaultConfig() @@ -609,7 +694,6 @@ func TestMatchByImage(t *testing.T) { } actualResults := grype.FindVulnerabilitiesForPackage(str, theDistro, matchers, pkg.FromCollection(collection, pkg.SynthesisConfig{})) - for _, m := range actualResults.Sorted() { for _, d := range m.Details { observedMatchers.Add(string(d.Matcher)) @@ -617,9 +701,52 @@ 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, collection, theStore) + + 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 + 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)) + } + } }) } @@ -640,6 +767,95 @@ func TestMatchByImage(t *testing.T) { } +// 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{ + 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: []wfn.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.StereoscopeImageSourceMetadata{ + RepoDigests: []string{ + "alpine@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }, + }, + Distro: &linux.Release{}, + } + + 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{ diff --git a/test/integration/match_by_sbom_document_test.go b/test/integration/match_by_sbom_document_test.go index 61e239b4ecc..a76b5365f8c 100644 --- a/test/integration/match_by_sbom_document_test.go +++ b/test/integration/match_by_sbom_document_test.go @@ -55,17 +55,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, 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..78b24f5b80b --- /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 b76686e4a9d..4f5a4098f74 100644 --- a/test/integration/utils_test.go +++ b/test/integration/utils_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/scylladb/go-set/strset" + "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/syft/syft" @@ -70,28 +71,31 @@ func saveImage(t testing.TB, imageName string, destPath string) { } func getSyftSBOM(t testing.TB, image string, format sbom.Format) string { - sourceInput, err := source.ParseInput(image, "") + detection, err := source.Detect(image, source.DetectConfig{}) if err != nil { t.Fatalf("could not generate source input for packages command: %+v", err) } - src, cleanup, err := source.New(*sourceInput, nil, nil) + src, err := detection.NewSource(source.DetectionSourceConfig{}) if err != nil { t.Fatalf("can't get the source: %+v", err) } - t.Cleanup(cleanup) + t.Cleanup(func() { + require.NoError(t, src.Close()) + }) config := cataloger.DefaultConfig() config.Search.Scope = source.SquashedScope // TODO: relationships are not verified at this time - collection, _, distro, err := syft.CatalogPackages(src, config) + collection, relationships, distro, err := syft.CatalogPackages(src, config) s := sbom.SBOM{ Artifacts: sbom.Artifacts{ Packages: collection, LinuxDistribution: distro, }, - Source: src.Metadata, + Relationships: relationships, + Source: src.Describe(), } bytes, err := syft.Encode(s, format) diff --git a/test/quality/.gitignore b/test/quality/.gitignore index 92d889aa0b7..efadd96d966 100644 --- a/test/quality/.gitignore +++ b/test/quality/.gitignore @@ -4,4 +4,5 @@ venv stage pull migrate.py -.oras-cache \ No newline at end of file +.oras-cache +*.tar.gz \ No newline at end of file diff --git a/test/quality/.yardstick.yaml b/test/quality/.yardstick.yaml index 1ed3389042d..dfae62e6f4d 100644 --- a/test/quality/.yardstick.yaml +++ b/test/quality/.yardstick.yaml @@ -32,7 +32,6 @@ x-ref: - docker.io/anchore/test_images:alpine-package-cpe-vuln-match-bd0aaef@sha256:0825acea611c7c5cc792bc7cc20de44d7413fd287dc5afc4aab9c1891d037b4f - docker.io/alpine:3.2@sha256:ddac200f3ebc9902fb8cfcd599f41feb2151f1118929da21bcef57dc276975f9 - docker.io/centos:6@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf - - docker.io/ubuntu:16.10@sha256:8dc9652808dc091400d7d5983949043a9f9c7132b15c14814275d25f94bca18a - docker.io/almalinux:8@sha256:cd49d7250ed7bb194d502d8a3e50bd775055ca275d1d9c2785aea72b890afe6a - docker.io/rockylinux:8@sha256:72afc2e1a20c9ddf56a81c51148ebcbe927c0a879849efe813bee77d69df1dd8 - docker.io/oraclelinux:6@sha256:a06327c0f1d18d753f2a60bb17864c84a850bb6dcbcf5946dd1a8123f6e75495 @@ -42,7 +41,7 @@ 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:14.1.0-slim@sha256:d8a88e8e15fd26eee7734c9f60531b5ad2abdc3d663be0d818ed26159db80512 + - 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 @@ -52,6 +51,33 @@ x-ref: - docker.io/postgres:13.2@sha256:1a67ab960138c479d66834cd6bcb5b5582c53869e6052dbf4ff48d4a94c13da3 - ghcr.io/chainguard-images/scanner-test@sha256:59bddc101fba0c45d5c093575c6bc5bfee7f0e46ff127e6bb4e5acaaafb525f9 - docker.io/keycloak/keycloak:21.0.2@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50 + - docker.io/datawire/aes:3.6.0@sha256:86a072278135462b6cbef70e89894df8f9b20f428b361fda2132fbb442ef257b + - docker.io/bitnami/spark:3.2.4-debian-11-r8@sha256:267d5a6345636710b4b57b7fe981c9760203e7e092c705416310ea30a9806d74 + - mcr.microsoft.com/cbl-mariner/base/core:2.0.20220731-arm64@sha256:51101e635f56032d5afd3fb56d66c7b93b34d5a39ddac01695d62b94473cc34e + - 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:14.04@sha256:881afbae521c910f764f7187dbfbca3cc10c26f8bafa458c76dda009a901c29d + - 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 + - docker.io/ubuntu:17.10@sha256:9c4bf7dbb981591d4a1169138471afe4bf5ff5418841d00e30a7ba372e38d6c1 + - docker.io/ubuntu:18.04@sha256:971a12d7e92a23183dead8bfc415aa650e7deb1cc5fed11a3d21f759a891fde9 + - docker.io/ubuntu:18.10@sha256:c95b7b93ccd48c3bfd97f8cac6d5ca8053ced584c9e8e6431861ca30b0d73114 + - docker.io/ubuntu:19.04@sha256:3db17bfc30b41cc18552578f4a66d7010050eb9fdc42bf6c3d82bb0dcdf88d58 + - docker.io/ubuntu:19.10@sha256:6852f9e05c5bce8aa77173fa83ce611f69f271ee3a16503c5f80c199969fd1eb + - docker.io/ubuntu:20.04@sha256:9d42d0e3e57bc067d10a75ee33bdd1a5298e95e5fc3c5d1fce98b455cb879249 + - docker.io/ubuntu:20.10@sha256:754eb641a1ba98a8b483c3595a14164fa4ed7f4b457e1aa05f13ce06f8151723 + - docker.io/ubuntu:21.04@sha256:cb92f03e258f965442b883f5402b310dd7a5ea0a661a865ad02a42bc21234bf7 + - docker.io/ubuntu:21.10@sha256:253908b2844746ab3f3a08fc8a44b9b9fc1efc408d5969b093ab9ffa11eb1894 + - docker.io/ubuntu:22.04@sha256:aa6c2c047467afc828e77e306041b7fa4a65734fe3449a54aa9c280822b0d87d + - docker.io/ubuntu:22.10@sha256:80fb4ea0c0a384a3072a6be1879c342bb636b0d105209535ba893ba75ab38ede + - docker.io/ubuntu:23.04@sha256:09f035f46361d193ded647342903b413d57d05cc06acff8285f9dda9f2d269d5 + - gcr.io/distroless/python3-debian11@sha256:69ae7f133d33faab720af28e78fb45707b623bcbc94ae02a07c633bf053f4b40 # 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. @@ -67,14 +93,26 @@ 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: v0.89.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.gz + # for local build of grype, use for example: + # version: path:../../ takes: SBOM - 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.gz takes: SBOM diff --git a/test/quality/Makefile b/test/quality/Makefile index 65e1c96c859..fb81557bbe4 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 = https://toolbox-data.anchore.io/grype/databases/vulnerability-db_v5_2023-08-24T01:23:40Z_fd07204627d474f68f90.tar.gz +TEST_DB = db.tar.gz +LISTING_FILE = https://toolbox-data.anchore.io/grype/databases/listing.json + # formatting variables BOLD := $(shell tput -T linux bold) PURPLE := $(shell tput -T linux setaf 5) @@ -21,22 +26,31 @@ 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 +validate: venv $(VULNERABILITY_LABELS)/Makefile ## Run all quality checks against already collected data $(ACTIVATE_VENV) ./gate.py .PHONY: capture capture: sboms vulns ## Collect and store all syft and grype results .PHONY: capture -vulns: venv ## Collect and store all grype results +vulns: venv $(TEST_DB) check-db ## Collect and store all grype results $(YARDSTICK) -v result capture -r $(RESULT_SET) +.PHONY: check-db +check-db: + @echo "Looking for test DB within the hosted listing file (which prunes DBs older that 90 days or the last 90 objects)" + @curl -sSL $(LISTING_FILE) | jq '.available[][] | select(.url == "$(TEST_DB_URL)") ' --exit-status || (echo "$(RED)DB is too stale to be used for testing. Please re-pin with a more up-to-date version.$(RESET)" && false) + @echo "DB is fresh enough to be used for testing!" + +$(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)" .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)" @@ -47,12 +61,11 @@ venv/touchfile: requirements.txt $(ACTIVATE_VENV) pip install -Ur requirements.txt touch venv/touchfile - $(YARDSTICK_RESULT_DIR): mkdir -p $(YARDSTICK_RESULT_DIR) -$(VULNERABILITY_LABELS): - git submodule update vulnerability-match-labels +$(VULNERABILITY_LABELS)/Makefile: + git submodule update --init .PHONY: clear-results clear-results: venv ## Clear all existing yardstick results diff --git a/test/quality/gate.py b/test/quality/gate.py index 3d615f98d16..74a75f557c7 100755 --- a/test/quality/gate.py +++ b/test/quality/gate.py @@ -60,24 +60,32 @@ def passed(self): return len(self.reasons) == 0 def guess_tool_orientation(tools: list[str]): + """ + Given a pair of tools, guess which is latest version, and which is the one + being compared to the latest version. + Returns (latest_tool, current_tool) + """ if len(tools) != 2: raise RuntimeError("expected 2 tools, got %s" % tools) + tool_a, tool_b = sorted(tools) + if tool_a == tool_b: + raise ValueError("latest release tool and current tool are the same") + if tool_a.endswith("latest"): + return tool_a, tool_b + elif tool_b.endswith("latest"): + return tool_b, tool_a - current_tool = None - latest_release_tool = None - for tool in tools: - if tool.endswith("latest"): - latest_release_tool = tool - continue - current_tool = tool + if "@path:" in tool_a and "@path:" not in tool_b: + # tool_a is a local build, so compare it against tool_b + return tool_b, tool_a + + if "@path:" in tool_b and "@path:" not in tool_a: + # tool_b is a local build, so compare it against tool_a + return tool_a, tool_b + + return tool_a, tool_b - 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' @@ -90,6 +98,17 @@ class bcolors: UNDERLINE = '\033[4m' RESET = '\033[0m' +if not sys.stdout.isatty(): + bcolors.HEADER = "" + bcolors.OKBLUE = "" + bcolors.OKCYAN = "" + bcolors.OKGREEN = "" + bcolors.WARNING = "" + bcolors.FAIL = "" + bcolors.BOLD = "" + bcolors.UNDERLINE = "" + bcolors.RESET = "" + def show_results_used(results: list[artifact.ScanResult]): print(f" Results used:") for idx, result in enumerate(results): @@ -323,4 +342,4 @@ def setup_logging(verbosity: int): ) if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/test/quality/requirements.txt b/test/quality/requirements.txt index 22018744933..63d706f57b5 100644 --- a/test/quality/requirements.txt +++ b/test/quality/requirements.txt @@ -1,3 +1,3 @@ -git+https://github.com/anchore/yardstick@b24e38cb80212c5fb58dc7a4fb83b7162df4a5b2 +git+https://github.com/anchore/yardstick@v0.8.0 # ../../../yardstick tabulate==0.9.0 diff --git a/test/quality/vulnerability-match-labels b/test/quality/vulnerability-match-labels index 5f5bfa96fbe..3e6c878d144 160000 --- a/test/quality/vulnerability-match-labels +++ b/test/quality/vulnerability-match-labels @@ -1 +1 @@ -Subproject commit 5f5bfa96fbe993649fa2c5eba81c788d1cc263e2 +Subproject commit 3e6c878d144f95aab8bbb398ad0e7c717d6c3c31 diff --git a/ui/event_handlers.go b/ui/event_handlers.go deleted file mode 100644 index 92f7b35a55b..00000000000 --- a/ui/event_handlers.go +++ /dev/null @@ -1,249 +0,0 @@ -package ui - -import ( - "context" - "fmt" - "io" - "sort" - "strings" - "sync" - "time" - - "github.com/dustin/go-humanize" - "github.com/gookit/color" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/go-progress" - "github.com/wagoodman/go-progress/format" - "github.com/wagoodman/jotframe/pkg/frame" - - grypeEventParsers "github.com/anchore/grype/grype/event/parsers" - "github.com/anchore/grype/grype/matcher" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/grype/internal/ui/components" - syftUI "github.com/anchore/syft/ui" -) - -const maxBarWidth = 50 -const statusSet = components.SpinnerDotSet // SpinnerCircleOutlineSet -const completedStatus = "✔" // "●" -const tileFormat = color.Bold - -var ( - auxInfoFormat = color.HEX("#777777") - statusTitleTemplate = fmt.Sprintf(" %%s %%-%ds ", syftUI.StatusTitleColumn) -) - -func startProcess() (format.Simple, *components.Spinner) { - width, _ := frame.GetTerminalSize() - barWidth := int(0.25 * float64(width)) - if barWidth > maxBarWidth { - barWidth = maxBarWidth - } - formatter := format.NewSimpleWithTheme(barWidth, format.HeavyNoBarTheme, format.ColorCompleted, format.ColorTodo) - spinner := components.NewSpinner(statusSet) - - return formatter, &spinner -} - -func (r *Handler) UpdateVulnerabilityDatabaseHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - prog, err := grypeEventParsers.ParseUpdateVulnerabilityDatabase(event) - if err != nil { - return fmt.Errorf("bad FetchImage event: %w", err) - } - - line, err := fr.Prepend() - if err != nil { - return err - } - - wg.Add(1) - - formatter, spinner := startProcess() - stream := progress.Stream(ctx, prog, 150*time.Millisecond) - title := tileFormat.Sprint("Vulnerability DB") - - formatFn := func(p progress.Progress) { - progStr, err := formatter.Format(p) - spin := color.Magenta.Sprint(spinner.Next()) - if err != nil { - _, _ = io.WriteString(line, fmt.Sprintf("Error: %+v", err)) - } else { - var auxInfo string - switch prog.Stage() { - case "downloading": - progStr += " " - auxInfo = auxInfoFormat.Sprintf(" [%s / %s]", humanize.Bytes(uint64(prog.Current())), humanize.Bytes(uint64(prog.Size()))) - default: - progStr = "" - auxInfo = auxInfoFormat.Sprintf("[%s]", prog.Stage()) - } - - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s%s", spin, title, progStr, auxInfo)) - } - } - - go func() { - defer wg.Done() - - formatFn(progress.Progress{}) - for p := range stream { - formatFn(p) - } - - spin := color.Green.Sprint(completedStatus) - title = tileFormat.Sprint("Vulnerability DB") - auxInfo := auxInfoFormat.Sprintf("[%s]", prog.Stage()) - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - }() - return err -} - -func scanningAndSummaryLines(fr *frame.Frame) (scanningLine, summaryLine, fixedLine *frame.Line, err error) { - scanningLine, err = fr.Append() - if err != nil { - return nil, nil, nil, err - } - - summaryLine, err = fr.Append() - if err != nil { - return nil, nil, nil, err - } - - fixedLine, err = fr.Append() - if err != nil { - return nil, nil, nil, err - } - return scanningLine, summaryLine, fixedLine, nil -} - -func assembleProgressMonitors(m *matcher.Monitor) []progress.Monitorable { - ret := []progress.Monitorable{ - m.PackagesProcessed, - m.VulnerabilitiesDiscovered, - } - - allSeverities := append([]vulnerability.Severity{vulnerability.UnknownSeverity}, vulnerability.AllSeverities()...) - for _, sev := range allSeverities { - ret = append(ret, m.BySeverity[sev]) - } - - ret = append(ret, m.Fixed) - - return ret -} - -//nolint:funlen -func (r *Handler) VulnerabilityScanningStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - monitor, err := grypeEventParsers.ParseVulnerabilityScanningStarted(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - scanningLine, summaryLine, fixLine, err := scanningAndSummaryLines(fr) - if err != nil { - return err - } - - wg.Add(1) - - monitors := assembleProgressMonitors(monitor) - - _, spinner := startProcess() - stream := progress.StreamMonitors(ctx, monitors, 50*time.Millisecond) - - title := tileFormat.Sprint("Scanning image...") - branch := "├──" - end := "└──" - - fixTempl := "%d fixed" - - formatFn := func(m *matcher.Monitor, complete bool) { - var spin string - if complete { - spin = color.Green.Sprint(completedStatus) - } else { - spin = color.Magenta.Sprint(spinner.Next()) - } - - auxInfo := auxInfoFormat.Sprintf("[%d vulnerabilities]", m.VulnerabilitiesDiscovered.Current()) - _, _ = io.WriteString(scanningLine, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - - var unknownStr string - unknown := m.BySeverity[vulnerability.UnknownSeverity].Current() - if unknown > 0 { - unknownStr = fmt.Sprintf(" (%d unknown)", unknown) - } - - allSeverities := vulnerability.AllSeverities() - sort.Sort(sort.Reverse(vulnerability.Severities(allSeverities))) - - var builder strings.Builder - for idx, sev := range allSeverities { - count := m.BySeverity[sev].Current() - builder.WriteString(fmt.Sprintf("%d %s", count, sev)) - if idx < len(allSeverities)-1 { - builder.WriteString(", ") - } - } - builder.WriteString(unknownStr) - - status := builder.String() - auxInfo2 := auxInfoFormat.Sprintf(" %s %s", branch, status) - _, _ = io.WriteString(summaryLine, auxInfo2) - - fixStatus := fmt.Sprintf(fixTempl, m.Fixed.Current()) - _, _ = io.WriteString(fixLine, auxInfoFormat.Sprintf(" %s %s", end, fixStatus)) - } - - go func() { - defer wg.Done() - - formatFn(monitor, false) - for range stream { - formatFn(monitor, false) - } - formatFn(monitor, true) - }() - - return nil -} - -func (r *Handler) DatabaseDiffingStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - monitor, err := grypeEventParsers.ParseDatabaseDiffingStarted(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - line, err := fr.Append() - if err != nil { - return err - } - - wg.Add(1) - - _, spinner := startProcess() - stream := progress.StreamMonitors(ctx, []progress.Monitorable{monitor.RowsProcessed, monitor.DifferencesDiscovered}, 50*time.Millisecond) - title := tileFormat.Sprint("Diffing databases...") - - formatFn := func(val int64) { - spin := color.Magenta.Sprint(spinner.Next()) - auxInfo := auxInfoFormat.Sprintf("[differences %d]", val) - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - } - - go func() { - defer wg.Done() - - formatFn(0) - for p := range stream { - formatFn(p[1]) - } - - spin := color.Green.Sprint(completedStatus) - title = tileFormat.Sprint("Diff Complete") - auxInfo := auxInfoFormat.Sprintf("[%d differences]", monitor.DifferencesDiscovered.Current()) - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - }() - - return nil -} diff --git a/ui/handler.go b/ui/handler.go deleted file mode 100644 index ec244d20ee3..00000000000 --- a/ui/handler.go +++ /dev/null @@ -1,46 +0,0 @@ -package ui - -import ( - "context" - "sync" - - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" - - grypeEvent "github.com/anchore/grype/grype/event" - syftUI "github.com/anchore/syft/ui" -) - -type Handler struct { - syftHandler *syftUI.Handler -} - -func NewHandler() *Handler { - return &Handler{ - syftHandler: syftUI.NewHandler(), - } -} - -func (r *Handler) RespondsTo(event partybus.Event) bool { - switch event.Type { - case grypeEvent.VulnerabilityScanningStarted, - grypeEvent.UpdateVulnerabilityDatabase, - grypeEvent.DatabaseDiffingStarted: - return true - default: - return r.syftHandler.RespondsTo(event) - } -} - -func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - switch event.Type { - case grypeEvent.VulnerabilityScanningStarted: - return r.VulnerabilityScanningStartedHandler(ctx, fr, event, wg) - case grypeEvent.UpdateVulnerabilityDatabase: - return r.UpdateVulnerabilityDatabaseHandler(ctx, fr, event, wg) - case grypeEvent.DatabaseDiffingStarted: - return r.DatabaseDiffingStartedHandler(ctx, fr, event, wg) - default: - return r.syftHandler.Handle(ctx, fr, event, wg) - } -}