diff --git a/.chronicle.yaml b/.chronicle.yaml index 400437c89..b74726c41 100644 --- a/.chronicle.yaml +++ b/.chronicle.yaml @@ -1 +1,2 @@ enforce-v0: true # don't make breaking-change label bump major version before 1.0. +title: "" \ No newline at end of file diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index 10ef12956..d8308336b 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -1,14 +1,11 @@ name: "Bootstrap" + description: "Bootstrap all tools and dependencies" inputs: go-version: description: "Go version to install" required: true - default: "1.20.x" - use-go-cache: - description: "Restore go cache" - required: true - default: "true" + default: "1.21.x" cache-key-prefix: description: "Prefix all cache keys with this value" required: true @@ -24,49 +21,25 @@ 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 }} - name: Restore tool cache id: tool-cache - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: 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.yml b/.github/dependabot.yml index eb2c65955..271775b7b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,12 @@ version: 2 updates: + - package-ecosystem: gomod + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: - interval: daily - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: daily + interval: "daily" + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.github/workflows/benchmark-testing.yaml b/.github/workflows/benchmark-testing.yaml index d581850d8..3f50679d9 100644 --- a/.github/workflows/benchmark-testing.yaml +++ b/.github/workflows/benchmark-testing.yaml @@ -4,6 +4,9 @@ on: workflow_dispatch: pull_request: +permissions: + contents: read + jobs: Benchmark-Test: @@ -13,13 +16,14 @@ jobs: # we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter # the job by event. steps: - - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Bootstrap environment uses: ./.github/actions/bootstrap - name: Restore base benchmark result - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: test/results/benchmark-main.txt # use base sha for PR or new commit hash for main push in benchmark result key @@ -35,13 +39,13 @@ jobs: OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters echo "result=$OUTPUT" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: benchmark-test-results path: test/results/**/* - name: Update PR benchmark results comment - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@efaaab3fd41a9c3de579aba759d2552635e590fd #v2.8.0 continue-on-error: true with: header: benchmark diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c6ac1cdb1..004457e1e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,11 +14,17 @@ on: schedule: - cron: '0 0 * * 3' +permissions: + contents: read + jobs: analyze: name: Analyze runs-on: ubuntu-latest + permissions: + security-events: write + strategy: fail-fast: false matrix: @@ -30,11 +36,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 + + - name: Install Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0 + with: + go-version-file: go.mod # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de #v2.22.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -45,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de #v2.22.2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -59,4 +70,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de #v2.22.2 diff --git a/.github/workflows/oss-project-board-add.yaml b/.github/workflows/oss-project-board-add.yaml index b0d1fca00..0472de8e5 100644 --- a/.github/workflows/oss-project-board-add.yaml +++ b/.github/workflows/oss-project-board-add.yaml @@ -1,5 +1,8 @@ name: Add to OSS board +permissions: + contents: read + on: issues: types: @@ -9,7 +12,6 @@ on: - labeled jobs: - run: uses: "anchore/workflows/.github/workflows/oss-project-board-add.yaml@main" secrets: diff --git a/.github/workflows/release-homebrew.yaml b/.github/workflows/release-homebrew.yaml new file mode 100644 index 000000000..0ccacd17b --- /dev/null +++ b/.github/workflows/release-homebrew.yaml @@ -0,0 +1,25 @@ +name: "Release Homebrew" + +on: + release: + types: [published] + +jobs: + homebrew: + runs-on: ubuntu-20.04 + steps: + - name: Update Homebrew formula + uses: dawidd6/action-homebrew-bump-formula@d3667e5ae14df19579e4414897498e3e88f2f458 # v3.10.0 + with: + token: ${{ secrets.HOMEBREW_TOKEN }} + org: anchore + formula: syft + + - uses: 8398a7/action-slack@fbd6aa58ba854a740e11a35d0df80cb5d12101d8 #v3.15.1 + if: ${{ failure() }} + with: + status: ${{ job.status }} + fields: repo,workflow,action,eventName + text: "Post-release homebrew publish workflow has failed: https://github.com/anchore/syft/actions/workflows/release-homebrew.yaml" + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a2cbd05ac..fc958856e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,4 +1,8 @@ name: "Release" + +permissions: + contents: read + on: workflow_dispatch: inputs: @@ -6,15 +10,12 @@ on: description: tag the latest commit on main with the given version (prefixed with v) required: true -env: - GO_VERSION: "1.19.x" - jobs: quality-gate: environment: release runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 - name: Check if tag already exists # note: this will fail if the tag already exists @@ -23,7 +24,7 @@ jobs: git tag ${{ github.event.inputs.version }} - name: Check static analysis results - uses: fountainhead/action-wait-for-check@v1.1.0 + uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0 id: static-analysis with: token: ${{ secrets.GITHUB_TOKEN }} @@ -32,7 +33,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check unit test results - uses: fountainhead/action-wait-for-check@v1.1.0 + uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0 id: unit with: token: ${{ secrets.GITHUB_TOKEN }} @@ -41,7 +42,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check integration test results - uses: fountainhead/action-wait-for-check@v1.1.0 + uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0 id: integration with: token: ${{ secrets.GITHUB_TOKEN }} @@ -50,7 +51,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check acceptance test results (linux) - uses: fountainhead/action-wait-for-check@v1.1.0 + uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0 id: acceptance-linux with: token: ${{ secrets.GITHUB_TOKEN }} @@ -59,7 +60,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check acceptance test results (mac) - uses: fountainhead/action-wait-for-check@v1.1.0 + uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0 id: acceptance-mac with: token: ${{ secrets.GITHUB_TOKEN }} @@ -68,7 +69,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check cli test results (linux) - uses: fountainhead/action-wait-for-check@v1.1.0 + uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0 id: cli-linux with: token: ${{ secrets.GITHUB_TOKEN }} @@ -94,7 +95,7 @@ jobs: contents: write packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 with: fetch-depth: 0 @@ -105,13 +106,13 @@ 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 }} @@ -119,7 +120,9 @@ jobs: - name: Tag release run: | - git tag ${{ github.event.inputs.version }} + git config --global user.name "anchoreci" + git config --global 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 }} @@ -141,12 +144,12 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }} - - uses: anchore/sbom-action@v0 + - uses: anchore/sbom-action@78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1 #v0.14.3 continue-on-error: true with: artifact-name: sbom.spdx.json - - uses: 8398a7/action-slack@v3 + - uses: 8398a7/action-slack@fbd6aa58ba854a740e11a35d0df80cb5d12101d8 #v3.15.1 continue-on-error: true with: status: ${{ job.status }} @@ -155,9 +158,3 @@ jobs: env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} if: ${{ success() }} - - - uses: actions/upload-artifact@v3 - with: - name: artifacts - path: dist/**/* - diff --git a/.github/workflows/update-bootstrap-tools.yml b/.github/workflows/update-bootstrap-tools.yml index 3d5624275..997aa961a 100644 --- a/.github/workflows/update-bootstrap-tools.yml +++ b/.github/workflows/update-bootstrap-tools.yml @@ -6,17 +6,20 @@ on: workflow_dispatch: env: - GO_VERSION: "1.20.x" + GO_VERSION: "1.21.x" GO_STABLE_VERSION: true +permissions: + contents: read + jobs: update-bootstrap-tools: runs-on: ubuntu-latest if: github.repository == 'anchore/syft' # only run for main repo steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0 with: go-version: ${{ env.GO_VERSION }} stable: ${{ env.GO_STABLE_VERSION }} @@ -29,6 +32,7 @@ jobs: 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') COSIGN_LATEST_VERSION=$(go list -m -json github.com/sigstore/cosign/v2@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 @@ -39,6 +43,7 @@ jobs: 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/^(COSIGN_VERSION := ).*/\1'${COSIGN_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 +54,17 @@ jobs: echo "GOSIMPORTS=$GOSIMPORTS_LATEST_VERSION" >> $GITHUB_OUTPUT echo "YAJSV=$YAJSV_LATEST_VERSION" >> $GITHUB_OUTPUT echo "COSIGN=$COSIGN_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@v1 + - 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@v5 + - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 #v5.0.2 with: signoff: true delete-branch: true @@ -74,6 +80,7 @@ jobs: - [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 }}) - [cosign ${{ steps.latest-versions.outputs.COSIGN }}](https://github.com/sigstore/cosign/releases/tag/${{ steps.latest-versions.outputs.COSIGN }}) + - [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-cpe-dictionary-index.yml b/.github/workflows/update-cpe-dictionary-index.yml index c7541bbae..571b06e7e 100644 --- a/.github/workflows/update-cpe-dictionary-index.yml +++ b/.github/workflows/update-cpe-dictionary-index.yml @@ -5,8 +5,11 @@ on: workflow_dispatch: +permissions: + contents: read + env: - GO_VERSION: "1.20.x" + GO_VERSION: "1.21.x" GO_STABLE_VERSION: true jobs: @@ -14,9 +17,9 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'anchore/syft' # only run for main repo steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@3df4ab11eba7bda6032a0b82a6bb43b11571feac #v4.0.0 with: go-version: ${{ env.GO_VERSION }} stable: ${{ env.GO_STABLE_VERSION }} @@ -24,13 +27,13 @@ jobs: - run: | make generate-cpe-dictionary-index - - uses: tibdex/github-app-token@v1 + - 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@v5 + - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 #v5.0.2 with: signoff: true delete-branch: true diff --git a/.github/workflows/update-stereoscope-release.yml b/.github/workflows/update-stereoscope-release.yml index f2d09c238..b5fad265f 100644 --- a/.github/workflows/update-stereoscope-release.yml +++ b/.github/workflows/update-stereoscope-release.yml @@ -6,17 +6,20 @@ on: workflow_dispatch: env: - GO_VERSION: "1.20.x" + GO_VERSION: "1.21.x" GO_STABLE_VERSION: true +permissions: + contents: read + jobs: upgrade-stereoscope: runs-on: ubuntu-latest if: github.repository == 'anchore/syft' # only run for main repo steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0 with: go-version: ${{ env.GO_VERSION }} stable: ${{ env.GO_STABLE_VERSION }} @@ -32,13 +35,13 @@ jobs: echo "LATEST_VERSION=$LATEST_VERSION" >> $GITHUB_OUTPUT id: latest-version - - uses: tibdex/github-app-token@v1 + - 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@v5 + - 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 861d7634f..48220a111 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -7,14 +7,16 @@ on: branches: - main -jobs: +permissions: + contents: read +jobs: Static-Analysis: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Static analysis" runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 - name: Bootstrap environment uses: ./.github/actions/bootstrap @@ -28,37 +30,37 @@ jobs: name: "Unit tests" runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 - name: Bootstrap environment uses: ./.github/actions/bootstrap - name: Restore Java test-fixture cache - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint' ) }} - name: Restore RPM test-fixture cache - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: syft/pkg/cataloger/rpm/test-fixtures/rpms key: ${{ runner.os }}-unit-rpm-cache-${{ hashFiles( 'syft/pkg/cataloger/rpm/test-fixtures/rpms.fingerprint' ) }} - name: Restore go binary test-fixture cache - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }} - name: Restore binary cataloger test-fixture cache - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: syft/pkg/cataloger/binary/test-fixtures/classifiers/dynamic key: ${{ runner.os }}-unit-binary-cataloger-cache-${{ hashFiles( 'syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint' ) }} - name: Restore Kernel test-fixture cache - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: syft/pkg/cataloger/kernel/test-fixtures/cache key: ${{ runner.os }}-unit-kernel-cache-${{ hashFiles( 'syft/pkg/cataloger/kernel/test-fixtures/cache.fingerprint' ) }} @@ -72,7 +74,7 @@ jobs: name: "Integration tests" runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 - name: Bootstrap environment uses: ./.github/actions/bootstrap @@ -81,7 +83,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') }} @@ -95,13 +97,13 @@ jobs: name: "CLI tests (Linux)" runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 - 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') }} @@ -110,45 +112,16 @@ jobs: run: make cli -# Build-Snapshot-Artifacts: -# name: "Build snapshot artifacts" -# runs-on: ubuntu-20.04 -# steps: -# - uses: actions/checkout@v3 -# -# - name: Bootstrap environment -# uses: ./.github/actions/bootstrap -# with: -# # why have another build cache key? We don't want unit/integration/etc test build caches to replace -# # the snapshot build cache, which includes builds for all OSs and architectures. As long as this key is -# # unique from the build-cache-key-prefix in other CI jobs, we should be fine. -# # -# # note: ideally this value should match what is used in release (just to help with build times). -# build-cache-key-prefix: "snapshot" -# bootstrap-apt-packages: "" -# -# - name: Build snapshot artifacts -# run: make snapshot -# -# # 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 -# with: -# path: snapshot -# key: snapshot-build-${{ github.run_id }} - - Acceptance-Linux: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Acceptance tests (Linux)" # needs: [Build-Snapshot-Artifacts] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 # - 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 }} @@ -158,7 +131,7 @@ jobs: - 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') }} @@ -181,17 +154,17 @@ jobs: # needs: [Build-Snapshot-Artifacts] runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 # - 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') }} diff --git a/.gitignore b/.gitignore index d38c4eed3..6d9a30386 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,39 @@ +# local development tailoring go.work go.work.sum +.tool-versions + +# app configuration +/.syft.yaml + +# tool and bin directories +.tmp/ +bin/ /bin /.bin /build -CHANGELOG.md -VERSION -/test/results /dist /snapshot -.server/ + +# changelog generation +CHANGELOG.md +VERSION + +# IDE configuration .vscode/ +.idea/ +.server/ .history/ + +# test related *.fingerprint +/test/results +coverage.txt +*.log +test/integration/test-fixtures/**/go.sum + +# probable archives +.images *.tar *.jar *.war @@ -19,13 +41,7 @@ VERSION *.jpi *.hpi *.zip -.idea/ *.iml -*.log -.images -.tmp/ -coverage.txt -bin/ # Binaries for programs and plugins *.exe diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d4e05af8e..f31f769df 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -24,10 +24,10 @@ builds: -w -s -extldflags '-static' - -X github.com/anchore/syft/internal/version.version={{.Version}} - -X github.com/anchore/syft/internal/version.gitCommit={{.Commit}} - -X github.com/anchore/syft/internal/version.buildDate={{.Date}} - -X github.com/anchore/syft/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/syft diff --git a/DEVELOPING.md b/DEVELOPING.md index 3d35f4607..e8a5e6f1f 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -153,56 +153,47 @@ sequenceDiagram ### Syft Catalogers -##### Summary - -Catalogers are the way in which syft is able to identify and construct packages given some amount of source metadata. -For example, Syft can locate and process `package-lock.json` files when performing filesystem scans. -See: [how to specify file globs](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) -and an implementation of the [package-lock.json parser](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) for a quick review. +Catalogers are the way in which syft is able to identify and construct packages given a set a targeted list of files. +For example, a cataloger can ask syft for all `package-lock.json` files in order to parse and raise up javascript packages +(see [how file globs](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) and +[file parser functions](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) are used +for a quick example). From a high level catalogers have the following properties: -- They are independent from one another. The java cataloger has no idea of the processes, assumptions, or results of the python cataloger, for example. +- _They are independent from one another_. The java cataloger has no idea of the processes, assumptions, or results of the python cataloger, for example. -- They do not know what source is being analyzed. Are we analyzing a local directory? an image? if so, the squashed representation or all layers? The catalogers do not know the answers to these questions. Only that there is an interface to query for file paths and contents from an underlying "source" being scanned. +- _They do not know what source is being analyzed_. Are we analyzing a local directory? an image? if so, the squashed representation or all layers? The catalogers do not know the answers to these questions. Only that there is an interface to query for file paths and contents from an underlying "source" being scanned. + +- _Packages created by the cataloger should not be mutated after they are created_. There is one exception made for adding CPEs to a package after the cataloging phase, but that will most likely be moved back into the cataloger in the future. -- Packages created by the cataloger should not be mutated after they are created. There is one exception made for adding CPEs to a package after the cataloging phase, but that will most likely be moved back into the cataloger in the future. #### Building a new Cataloger -Catalogers must fulfill the interface [found here](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger.go). -This means that when building a new cataloger, the new struct must implement both method signatures of `Catalog` and `Name`. +Catalogers must fulfill the [`pkg.Cataloger` interface](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger.go) in order to add packages to the SBOM. +All catalogers should be added to: +- the [global list of catalogers](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/cataloger/cataloger.go#L92-L125) +- at least one source-specific list, today the two lists are [directory catalogers and image catalogers](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/cataloger/cataloger.go#L39-L89) -A top level view of the functions that construct all the catalogers can be found [here](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/cataloger.go). -When an author has finished writing a new cataloger this is the spot to plug in the new catalog constructor. +For reference, catalogers are [invoked within syft](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/catalog.go#L41-L100) one after the other, and can be invoked in parallel. -For a top level view of how the catalogers are used see [this function](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/catalog.go#L41-L100) as a reference. It ranges over all catalogers passed as an argument and invokes the `Catalog` method: +`generic.NewCataloger` is an abstraction syft used to make writing common components easier (see the [apkdb cataloger](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/cataloger.go) for example usage). +It takes the following information as input: +- A `catalogerName` to identify the cataloger uniquely among all other catalogers. +- Pairs of file globs as well as parser functions to parse those files. These parser functions return a slice of [`pkg.Package`](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/package.go#L19) as well as a slice of [`artifact.Relationship`](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/artifact/relationship.go#L31) to describe how the returned packages are related. See this [the apkdb cataloger parser function](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L22-L102) as an example. -Each cataloger has its own `Catalog` method, but this does not mean that they are all vastly different. -Take a look at the `apkdb` cataloger for alpine to see how it [constructs a generic.NewCataloger](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/cataloger.go). - -`generic.NewCataloger` is an abstraction syft uses to make writing common components easier. First, it takes the `catalogerName` to identify the cataloger. -On the other side of the call it uses two key pieces which inform the cataloger how to identify and return packages, the `globPatterns` and the `parseFunction`: -- The first piece is a `parseByGlob` matching pattern used to identify the files that contain the package metadata. -See [here for the APK example](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/apk_metadata.go#L16-L41). -- The other is a `parseFunction` which informs the cataloger what to do when it has found one of the above matches files. -See this [link for an example](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L22-L102). - -If you're unsure about using the `Generic Cataloger` and think the use case being filled requires something more custom -just file an issue or ask in our slack, and we'd be more than happy to help on the design. - -Identified packages share a common struct so be sure that when the new cataloger is constructing a new package it is using the [`Package` struct](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/package.go#L16-L31). - -Metadata Note: Identified packages are also assigned specific metadata that can be unique to their environment. -See [this folder](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg) for examples of the different metadata types. +Identified packages share a common `pkg.Package` struct so be sure that when the new cataloger is constructing a new package it is using the [`Package` struct](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/package.go#L16-L31). +If you want to return more information than what is available on the `pkg.Package` struct then you can do so in the `pkg.Package.Metadata` section of the struct, which is unique for each [`pkg.Type`](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/type.go). +See [the `pkg` package](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg) for examples of the different metadata types that are supported today. These are plugged into the `MetadataType` and `Metadata` fields in the above struct. `MetadataType` informs which type is being used. `Metadata` is an interface converted to that type. -Finally, here is an example of where the package construction is done in the apk cataloger. The first link is where `newPackage` is called in the `parseFunction`. The second link shows the package construction: -- [Call for new package](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L106) -- [APK Package Constructor](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/package.go#L12-L27) +Finally, here is an example of where the package construction is done within the apk cataloger: +- [Calling the APK package constructor from the parser function](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L106) +- [The APK package constructor itself](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/package.go#L12-L27) + +Interested in building a new cataloger? Checkout the [list of issues with the `new-cataloger` label](https://github.com/anchore/syft/issues?q=is%3Aopen+is%3Aissue+label%3Anew-cataloger+no%3Aassignee)! +If you have questions about implementing a cataloger feel free to file an issue or reach out to us [on slack](https://anchore.com/slack)! -If you have more questions about implementing a cataloger or questions about one you might be currently working -always feel free to file an issue or reach out to us [on slack](https://anchore.com/slack). #### Searching for files diff --git a/Dockerfile b/Dockerfile index c93c21fab..e922e3d56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gcr.io/distroless/static-debian11:debug AS build +FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951 AS build FROM scratch # needed for version check HTTPS request diff --git a/Dockerfile.debug b/Dockerfile.debug index 3c1761cf4..37a657b3f 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -1,4 +1,4 @@ -FROM gcr.io/distroless/static-debian11:debug +FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951 # create the /tmp dir, which is needed for image content cache WORKDIR /tmp diff --git a/Makefile b/Makefile index 46b2c9b1d..31eb24586 100644 --- a/Makefile +++ b/Makefile @@ -10,14 +10,14 @@ CHRONICLE_CMD = $(TEMP_DIR)/chronicle GLOW_CMD = $(TEMP_DIR)/glow # Tool versions ################################# -GOLANGCILINT_VERSION := v1.54.0 +GOLANGCILINT_VERSION := v1.54.2 GOSIMPORTS_VERSION := v0.3.8 BOUNCER_VERSION := v0.4.0 -CHRONICLE_VERSION := v0.7.0 -GORELEASER_VERSION := v1.20.0 +CHRONICLE_VERSION := v0.8.0 +GORELEASER_VERSION := v1.21.2 YAJSV_VERSION := v1.4.1 -COSIGN_VERSION := v2.1.1 -QUILL_VERSION := v0.2.0 +COSIGN_VERSION := v2.2.0 +QUILL_VERSION := v0.4.1 GLOW_VERSION := v1.5.1 # Formatting variables ################################# @@ -146,13 +146,14 @@ check-json-schema-drift: .PHONY: unit unit: $(TEMP_DIR) fixtures ## 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/syft/test) + go test -race -coverprofile $(TEMP_DIR)/unit-coverage-details.txt $(shell go list ./... | grep -v anchore/syft/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 -race cmd/syft/main.go alpine:latest .PHONY: validate-cyclonedx-schema validate-cyclonedx-schema: diff --git a/README.md b/README.md index f1e6970e0..4aa685b5a 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ Sources can be explicitly provided with a scheme: ``` docker:yourrepo/yourimage:tag use images from the Docker daemon podman:yourrepo/yourimage:tag use images from the Podman daemon +containerd:yourrepo/yourimage:tag use images from the Containerd daemon docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save" oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise) oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise) @@ -389,11 +390,11 @@ syft convert -o [=] This feature is experimental and data might be lost when converting formats. Packages are the main SBOM component easily transferable across formats, whereas files and relationships, as well as other information Syft doesn't support, are more likely to be lost. We support formats with wide community usage AND good encode/decode support by Syft. The supported formats are: -- Syft JSON -- SPDX 2.2 JSON -- SPDX 2.2 tag-value -- CycloneDX 1.4 JSON -- CycloneDX 1.4 XML +- Syft JSON (```-o syft-json```) +- SPDX 2.2 JSON (```-o spdx-json```) +- SPDX 2.2 tag-value (```-o spdx-tag-value```) +- CycloneDX 1.4 JSON (```-o cyclonedx-json```) +- CycloneDX 1.4 XML (```-o cyclonedx-xml```) Conversion example: ```sh @@ -671,7 +672,7 @@ source: # the file digest algorithms to use on the scanned file (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512") digests: ["sha256"] -# options when pulling directly from a registry via the "registry:" scheme +# options when pulling directly from a registry via the "registry:" or "containerd:" scheme registry: # skip TLS verification when communicating with the registry # SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var @@ -681,19 +682,35 @@ registry: # SYFT_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 + # SYFT_REGISTRY_CA_CERT env var + ca-cert: "" + # credentials for specific registries auth: # the URL to the registry (e.g. "docker.io", "localhost:5000", etc.) # SYFT_REGISTRY_AUTH_AUTHORITY env var - authority: "" + # SYFT_REGISTRY_AUTH_USERNAME env var username: "" + # SYFT_REGISTRY_AUTH_PASSWORD env var password: "" + # note: token and username/password are mutually exclusive # SYFT_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 + # SYFT_REGISTRY_AUTH_TLS_CERT env var + tls-cert: "" + + # filepath to the client key used for TLS authentication to the registry + # SYFT_REGISTRY_AUTH_TLS_KEY env var + tls-key: "" + + # - ... # note, more credentials can be provided via config file only (not env vars) # generate an attested SBOM attest: diff --git a/cmd/syft/cli/attest.go b/cmd/syft/cli/attest.go deleted file mode 100644 index 0234057fe..000000000 --- a/cmd/syft/cli/attest.go +++ /dev/null @@ -1,66 +0,0 @@ -package cli - -import ( - "fmt" - "log" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/anchore/syft/cmd/syft/cli/attest" - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/config" -) - -const ( - attestExample = ` {{.appName}} {{.command}} --output [FORMAT] alpine:latest defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry -` - attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp - attestHelp = attestExample + attestSchemeHelp -) - -func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions, ao *options.AttestOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "attest --output [FORMAT] ", - Short: "Generate an SBOM as an attestation for the given [SOURCE] container image", - Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation that will be uploaded to the image registry", - Example: internal.Tprintf(attestHelp, map[string]interface{}{ - "appName": internal.ApplicationName, - "command": "attest", - }), - Args: func(cmd *cobra.Command, args []string) error { - if err := app.LoadAllValues(v, ro.Config); err != nil { - return fmt.Errorf("unable to load configuration: %w", err) - } - - newLogWrapper(app) - logApplicationConfig(app) - return validateArgs(cmd, args) - }, - SilenceUsage: true, - SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { - if app.CheckForAppUpdate { - checkForApplicationUpdate() - // TODO: this is broke, the bus isn't available yet - } - - return attest.Run(cmd.Context(), app, args) - }, - } - - // syft attest is an enhancement of the packages command, so it should have the same flags - err := po.AddFlags(cmd, v) - if err != nil { - log.Fatal(err) - } - - // syft attest has its own options not included as part of the packages command - err = ao.AddFlags(cmd, v) - if err != nil { - log.Fatal(err) - } - - return cmd -} diff --git a/cmd/syft/cli/attest/attest.go b/cmd/syft/cli/attest/attest.go deleted file mode 100644 index 758af6706..000000000 --- a/cmd/syft/cli/attest/attest.go +++ /dev/null @@ -1,261 +0,0 @@ -package attest - -import ( - "context" - "fmt" - "os" - "os/exec" - "strings" - - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/go-progress" - "golang.org/x/exp/slices" - - "github.com/anchore/stereoscope" - "github.com/anchore/stereoscope/pkg/image" - "github.com/anchore/syft/cmd/syft/cli/eventloop" - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/cmd/syft/cli/packages" - "github.com/anchore/syft/cmd/syft/internal/ui" - "github.com/anchore/syft/internal/bus" - "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/internal/file" - "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/event" - "github.com/anchore/syft/syft/event/monitor" - "github.com/anchore/syft/syft/formats/syftjson" - "github.com/anchore/syft/syft/formats/table" - "github.com/anchore/syft/syft/sbom" - "github.com/anchore/syft/syft/source" -) - -func Run(_ context.Context, app *config.Application, args []string) error { - err := ValidateOutputOptions(app) - if err != nil { - return err - } - - // note: must be a container image - userInput := args[0] - - _, err = exec.LookPath("cosign") - if err != nil { - // when cosign is not installed the error will be rendered like so: - // 2023/06/30 08:31:52 error during command execution: 'syft attest' requires cosign to be installed: exec: "cosign": executable file not found in $PATH - return fmt.Errorf("'syft attest' requires cosign to be installed: %w", err) - } - - eventBus := partybus.NewBus() - stereoscope.SetBus(eventBus) - syft.SetBus(eventBus) - subscription := eventBus.Subscribe() - - return eventloop.EventLoop( - execWorker(app, userInput), - eventloop.SetupSignals(), - subscription, - stereoscope.Cleanup, - ui.Select(options.IsVerbose(app), app.Quiet)..., - ) -} - -func buildSBOM(app *config.Application, userInput string, errs chan error) (*sbom.SBOM, error) { - cfg := source.DetectConfig{ - DefaultImageSource: app.DefaultImagePullSource, - } - detection, err := source.Detect(userInput, cfg) - if err != nil { - return nil, fmt.Errorf("could not deteremine source: %w", err) - } - - if detection.IsContainerImage() { - return nil, fmt.Errorf("attestations are only supported for oci images at this time") - } - - var platform *image.Platform - - if app.Platform != "" { - platform, err = image.NewPlatform(app.Platform) - if err != nil { - return nil, fmt.Errorf("invalid platform: %w", err) - } - } - - hashers, err := file.Hashers(app.Source.File.Digests...) - if err != nil { - return nil, fmt.Errorf("invalid hash: %w", err) - } - - src, err := detection.NewSource( - source.DetectionSourceConfig{ - Alias: source.Alias{ - Name: app.Source.Name, - Version: app.Source.Version, - }, - RegistryOptions: app.Registry.ToOptions(), - Platform: platform, - Exclude: source.ExcludeConfig{ - Paths: app.Exclusions, - }, - DigestAlgorithms: hashers, - BasePath: app.BasePath, - }, - ) - - if src != nil { - defer src.Close() - } - if err != nil { - return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) - } - - s, err := packages.GenerateSBOM(src, errs, app) - if err != nil { - return nil, err - } - - if s == nil { - return nil, fmt.Errorf("no SBOM produced for %q", userInput) - } - - return s, nil -} - -//nolint:funlen -func execWorker(app *config.Application, userInput string) <-chan error { - errs := make(chan error) - go func() { - defer close(errs) - defer bus.Exit() - - s, err := buildSBOM(app, userInput, errs) - if err != nil { - errs <- fmt.Errorf("unable to build SBOM: %w", err) - return - } - - // note: ValidateOutputOptions ensures that there is no more than one output type - o := app.Outputs[0] - - f, err := os.CreateTemp("", o) - if err != nil { - errs <- fmt.Errorf("unable to create temp file: %w", err) - return - } - defer os.Remove(f.Name()) - - writer, err := options.MakeSBOMWriter(app.Outputs, f.Name(), app.OutputTemplatePath) - if err != nil { - errs <- fmt.Errorf("unable to create SBOM writer: %w", err) - return - } - - if err := writer.Write(*s); err != nil { - errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err) - return - } - - // TODO: what other validation here besides binary name? - cmd := "cosign" - if !commandExists(cmd) { - errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed") - return - } - - // Select Cosign predicate type based on defined output type - // As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go - var predicateType string - switch strings.ToLower(o) { - case "cyclonedx-json": - predicateType = "cyclonedx" - case "spdx-tag-value", "spdx-tv": - predicateType = "spdx" - case "spdx-json", "json": - predicateType = "spdxjson" - default: - predicateType = "custom" - } - - args := []string{"attest", userInput, "--predicate", f.Name(), "--type", predicateType} - if app.Attest.Key != "" { - args = append(args, "--key", app.Attest.Key) - } - - execCmd := exec.Command(cmd, args...) - execCmd.Env = os.Environ() - if app.Attest.Key != "" { - execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", app.Attest.Password)) - } else { - // no key provided, use cosign's keyless mode - execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1") - } - - log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation") - - // bus adapter for ui to hook into stdout via an os pipe - r, w, err := os.Pipe() - if err != nil { - errs <- fmt.Errorf("unable to create os pipe: %w", err) - return - } - defer w.Close() - - mon := progress.NewManual(-1) - - bus.Publish( - partybus.Event{ - Type: event.AttestationStarted, - Source: monitor.GenericTask{ - Title: monitor.Title{ - Default: "Create attestation", - WhileRunning: "Creating attestation", - OnSuccess: "Created attestation", - }, - Context: "cosign", - }, - Value: &monitor.ShellProgress{ - Reader: r, - Progressable: mon, - }, - }, - ) - - execCmd.Stdout = w - execCmd.Stderr = w - - // attest the SBOM - err = execCmd.Run() - if err != nil { - mon.SetError(err) - errs <- fmt.Errorf("unable to attest SBOM: %w", err) - return - } - - mon.SetCompleted() - }() - return errs -} - -func ValidateOutputOptions(app *config.Application) error { - err := packages.ValidateOutputOptions(app) - if err != nil { - return err - } - - if len(app.Outputs) > 1 { - return fmt.Errorf("multiple SBOM format is not supported for attest at this time") - } - - // cannot use table as default output format when using template output - if slices.Contains(app.Outputs, table.ID.String()) { - app.Outputs = []string{syftjson.ID.String()} - } - - return nil -} - -func commandExists(cmd string) bool { - _, err := exec.LookPath(cmd) - return err == nil -} diff --git a/cmd/syft/cli/cli.go b/cmd/syft/cli/cli.go new file mode 100644 index 000000000..4f4608a66 --- /dev/null +++ b/cmd/syft/cli/cli.go @@ -0,0 +1,96 @@ +package cli + +import ( + "os" + + cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd" + "github.com/spf13/cobra" + + "github.com/anchore/clio" + "github.com/anchore/stereoscope" + "github.com/anchore/syft/cmd/syft/cli/commands" + handler "github.com/anchore/syft/cmd/syft/cli/ui" + "github.com/anchore/syft/cmd/syft/internal/ui" + "github.com/anchore/syft/internal/bus" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal/redact" +) + +// Application constructs the `syft packages` command, aliases the root command to `syft packages`, +// and constructs the `syft power-user` command. It is also responsible for +// organizing flag usage and injecting the application config for each command. +// It also constructs the syft attest command and the syft version command. +// `RunE` is the earliest that the complete application configuration can be loaded. +func Application(id clio.Identification) clio.Application { + app, _ := create(id) + return app +} + +// Command returns the root command for the syft CLI application. This is useful for embedding the entire syft CLI +// into an existing application. +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, + handler.New(handler.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) + bus.Set(state.Bus) + + redact.Set(state.RedactStore) + + log.Set(state.Logger) + stereoscope.SetLogger(state.Logger) + + return nil + }, + ). + WithPostRuns(func(state *clio.State, err error) { + stereoscope.Cleanup() + }) + + app := clio.New(*clioCfg) + + // since root is aliased as the packages cmd we need to construct this command first + // we also need the command to have information about the `root` options because of this alias + packagesCmd := commands.Packages(app) + + // rootCmd is currently an alias for the packages command + rootCmd := commands.Root(app, packagesCmd) + + // add sub-commands + rootCmd.AddCommand( + packagesCmd, + commands.PowerUser(app), + commands.Attest(app), + commands.Convert(app), + clio.VersionCommand(id), + cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane + ) + + return app, rootCmd +} diff --git a/cmd/syft/cli/commands.go b/cmd/syft/cli/commands.go deleted file mode 100644 index aa64d5a62..000000000 --- a/cmd/syft/cli/commands.go +++ /dev/null @@ -1,167 +0,0 @@ -package cli - -import ( - "fmt" - "strings" - - cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd" - "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/stereoscope" - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/bus" - "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/internal/version" - "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/event" -) - -const indent = " " - -// New constructs the `syft packages` command, aliases the root command to `syft packages`, -// and constructs the `syft power-user` command. It is also responsible for -// organizing flag usage and injecting the application config for each command. -// It also constructs the syft attest command and the syft version command. - -// Because of how the `cobra` library behaves, the application's configuration is initialized -// at this level. Values from the config should only be used after `app.LoadAllValues` has been called. -// Cobra does not have knowledge of the user provided flags until the `RunE` block of each command. -// `RunE` is the earliest that the complete application configuration can be loaded. -func New() (*cobra.Command, error) { - app := &config.Application{} - - // allow for nested options to be specified via environment variables - // e.g. pod.context = APPNAME_POD_CONTEXT - v := viper.NewWithOptions(viper.EnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))) - - // since root is aliased as the packages cmd we need to construct this command first - // we also need the command to have information about the `root` options because of this alias - ro := &options.RootOptions{} - po := &options.PackagesOptions{} - ao := &options.AttestOptions{} - packagesCmd := Packages(v, app, ro, po) - - // root options are also passed to the attestCmd so that a user provided config location can be discovered - poweruserCmd := PowerUser(v, app, ro) - convertCmd := Convert(v, app, ro, po) - attestCmd := Attest(v, app, ro, po, ao) - - // rootCmd is currently an alias for the packages command - rootCmd := &cobra.Command{ - Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName), - Short: packagesCmd.Short, - Long: packagesCmd.Long, - Args: packagesCmd.Args, - Example: packagesCmd.Example, - SilenceUsage: true, - SilenceErrors: true, - RunE: packagesCmd.RunE, - Version: version.FromBuild().Version, - } - rootCmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", internal.ApplicationName)) - - // start adding flags to all the commands - err := ro.AddFlags(rootCmd, v) - if err != nil { - return nil, err - } - // package flags need to be decorated onto the rootCmd so that rootCmd can function as a packages alias - err = po.AddFlags(rootCmd, v) - if err != nil { - return nil, err - } - - // poweruser also uses the packagesCmd flags since it is a specialized version of the command - err = po.AddFlags(poweruserCmd, v) - if err != nil { - return nil, err - } - - // commands to add to root - cmds := []*cobra.Command{ - packagesCmd, - poweruserCmd, - convertCmd, - attestCmd, - Version(v, app), - cranecmd.NewCmdAuthLogin("syft"), // syft login uses the same command as crane - } - - // Add sub-commands. - for _, cmd := range cmds { - rootCmd.AddCommand(cmd) - } - - return rootCmd, err -} - -func validateArgs(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - // in the case that no arguments are given 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 checkForApplicationUpdate() { - log.Debugf("checking if a new version of %s is available", internal.ApplicationName) - isAvailable, newVersion, err := version.IsUpdateAvailable() - if err != nil { - // this should never stop the application - log.Errorf(err.Error()) - } - if isAvailable { - log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version) - - bus.Publish(partybus.Event{ - Type: event.CLIAppUpdateAvailable, - Value: newVersion, - }) - } else { - log.Debugf("no new %s update available", internal.ApplicationName) - } -} - -func logApplicationConfig(app *config.Application) { - versionInfo := version.FromBuild() - log.Infof("%s version: %+v", internal.ApplicationName, versionInfo.Version) - log.Debugf("application config:\n%+v", color.Magenta.Sprint(app.String())) -} - -func newLogWrapper(app *config.Application) { - cfg := logrus.Config{ - EnableConsole: (app.Log.FileLocation == "" || app.Verbosity > 0) && !app.Quiet, - FileLocation: app.Log.FileLocation, - Level: app.Log.Level, - } - - if app.Log.Structured { - cfg.Formatter = &logrusUpstream.JSONFormatter{ - TimestampFormat: "2006-01-02 15:04:05", - 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 - } - syft.SetLogger(logWrapper) - stereoscope.SetLogger(logWrapper.Nested("from-lib", "stereoscope")) -} diff --git a/cmd/syft/cli/commands/attest.go b/cmd/syft/cli/commands/attest.go new file mode 100644 index 000000000..97ade2437 --- /dev/null +++ b/cmd/syft/cli/commands/attest.go @@ -0,0 +1,258 @@ +package commands + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/spf13/cobra" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/clio" + "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/cmd/syft/cli/options" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/bus" + "github.com/anchore/syft/internal/file" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/event" + "github.com/anchore/syft/syft/event/monitor" + "github.com/anchore/syft/syft/formats" + "github.com/anchore/syft/syft/formats/github" + "github.com/anchore/syft/syft/formats/syftjson" + "github.com/anchore/syft/syft/formats/table" + "github.com/anchore/syft/syft/formats/template" + "github.com/anchore/syft/syft/formats/text" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +const ( + attestExample = ` {{.appName}} {{.command}} --output [FORMAT] alpine:latest defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry +` + attestSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + attestHelp = attestExample + attestSchemeHelp +) + +type attestOptions struct { + options.Config `yaml:",inline" mapstructure:",squash"` + options.SingleOutput `yaml:",inline" mapstructure:",squash"` + options.UpdateCheck `yaml:",inline" mapstructure:",squash"` + options.Catalog `yaml:",inline" mapstructure:",squash"` + options.Attest `yaml:",inline" mapstructure:",squash"` +} + +func Attest(app clio.Application) *cobra.Command { + id := app.ID() + + var allowableOutputs []string + for _, f := range formats.AllIDs() { + switch f { + case table.ID, text.ID, github.ID, template.ID: + continue + } + allowableOutputs = append(allowableOutputs, f.String()) + } + + opts := &attestOptions{ + UpdateCheck: options.DefaultUpdateCheck(), + SingleOutput: options.SingleOutput{ + AllowableOptions: allowableOutputs, + Output: syftjson.ID.String(), + }, + Catalog: options.DefaultCatalog(), + } + + return app.SetupCommand(&cobra.Command{ + Use: "attest --output [FORMAT] ", + Short: "Generate an SBOM as an attestation for the given [SOURCE] container image", + Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation that will be uploaded to the image registry", + Example: internal.Tprintf(attestHelp, map[string]interface{}{ + "appName": id.Name, + "command": "attest", + }), + Args: validatePackagesArgs, + PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), + RunE: func(cmd *cobra.Command, args []string) error { + return runAttest(id, opts, args[0]) + }, + }, opts) +} + +//nolint:funlen +func runAttest(id clio.Identification, opts *attestOptions, userInput string) error { + _, err := exec.LookPath("cosign") + if err != nil { + // when cosign is not installed the error will be rendered like so: + // 2023/06/30 08:31:52 error during command execution: 'syft attest' requires cosign to be installed: exec: "cosign": executable file not found in $PATH + return fmt.Errorf("'syft attest' requires cosign to be installed: %w", err) + } + + s, err := buildSBOM(id, &opts.Catalog, userInput) + if err != nil { + return fmt.Errorf("unable to build SBOM: %w", err) + } + + o := opts.Output + + f, err := os.CreateTemp("", o) + if err != nil { + return fmt.Errorf("unable to create temp file: %w", err) + } + defer os.Remove(f.Name()) + + writer, err := opts.SBOMWriter(f.Name()) + if err != nil { + return fmt.Errorf("unable to create SBOM writer: %w", err) + } + + if err := writer.Write(*s); err != nil { + return fmt.Errorf("unable to write SBOM to temp file: %w", err) + } + + // TODO: what other validation here besides binary name? + cmd := "cosign" + if !commandExists(cmd) { + return fmt.Errorf("unable to find cosign in PATH; make sure you have it installed") + } + + // Select Cosign predicate type based on defined output type + // As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go + var predicateType string + switch strings.ToLower(o) { + case "cyclonedx-json": + predicateType = "cyclonedx" + case "spdx-tag-value", "spdx-tv": + predicateType = "spdx" + case "spdx-json", "json": + predicateType = "spdxjson" + default: + predicateType = "custom" + } + + args := []string{"attest", userInput, "--predicate", f.Name(), "--type", predicateType} + if opts.Attest.Key != "" { + args = append(args, "--key", opts.Attest.Key.String()) + } + + execCmd := exec.Command(cmd, args...) + execCmd.Env = os.Environ() + if opts.Attest.Key != "" { + execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", opts.Attest.Password)) + } else { + // no key provided, use cosign's keyless mode + execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1") + } + + log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation") + + // bus adapter for ui to hook into stdout via an os pipe + r, w, err := os.Pipe() + if err != nil { + return fmt.Errorf("unable to create os pipe: %w", err) + } + defer w.Close() + + mon := progress.NewManual(-1) + + bus.Publish( + partybus.Event{ + Type: event.AttestationStarted, + Source: monitor.GenericTask{ + Title: monitor.Title{ + Default: "Create attestation", + WhileRunning: "Creating attestation", + OnSuccess: "Created attestation", + }, + Context: "cosign", + }, + Value: &monitor.ShellProgress{ + Reader: r, + Progressable: mon, + }, + }, + ) + + execCmd.Stdout = w + execCmd.Stderr = w + + // attest the SBOM + err = execCmd.Run() + if err != nil { + mon.SetError(err) + return fmt.Errorf("unable to attest SBOM: %w", err) + } + + mon.SetCompleted() + + return nil +} + +func buildSBOM(id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) { + cfg := source.DetectConfig{ + DefaultImageSource: opts.DefaultImagePullSource, + } + detection, err := source.Detect(userInput, cfg) + if err != nil { + return nil, fmt.Errorf("could not deteremine source: %w", err) + } + + if detection.IsContainerImage() { + return nil, fmt.Errorf("attestations are only supported for oci images at this time") + } + + var platform *image.Platform + + if opts.Platform != "" { + platform, err = image.NewPlatform(opts.Platform) + if err != nil { + return nil, fmt.Errorf("invalid platform: %w", err) + } + } + + hashers, err := file.Hashers(opts.Source.File.Digests...) + if err != nil { + return nil, fmt.Errorf("invalid hash: %w", err) + } + + src, err := detection.NewSource( + source.DetectionSourceConfig{ + Alias: source.Alias{ + Name: opts.Source.Name, + Version: opts.Source.Version, + }, + RegistryOptions: opts.Registry.ToOptions(), + Platform: platform, + Exclude: source.ExcludeConfig{ + Paths: opts.Exclusions, + }, + DigestAlgorithms: hashers, + BasePath: opts.BasePath, + }, + ) + + if src != nil { + defer src.Close() + } + if err != nil { + return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) + } + + s, err := generateSBOM(id, src, opts) + if err != nil { + return nil, err + } + + if s == nil { + return nil, fmt.Errorf("no SBOM produced for %q", userInput) + } + + return s, nil +} + +func commandExists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} diff --git a/cmd/syft/cli/commands/convert.go b/cmd/syft/cli/commands/convert.go new file mode 100644 index 000000000..f42b7da62 --- /dev/null +++ b/cmd/syft/cli/commands/convert.go @@ -0,0 +1,95 @@ +package commands + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + "github.com/anchore/clio" + "github.com/anchore/syft/cmd/syft/cli/options" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/formats" +) + +const ( + convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout + {{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json"" + {{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json +` +) + +type ConvertOptions struct { + options.Config `yaml:",inline" mapstructure:",squash"` + options.MultiOutput `yaml:",inline" mapstructure:",squash"` + options.UpdateCheck `yaml:",inline" mapstructure:",squash"` +} + +//nolint:dupl +func Convert(app clio.Application) *cobra.Command { + id := app.ID() + + opts := &ConvertOptions{ + UpdateCheck: options.DefaultUpdateCheck(), + } + + return app.SetupCommand(&cobra.Command{ + Use: "convert [SOURCE-SBOM] -o [FORMAT]", + Short: "Convert between SBOM formats", + Long: "[Experimental] Convert SBOM files to, and from, SPDX, CycloneDX and Syft's format. For more info about data loss between formats see https://github.com/anchore/syft#format-conversion-experimental", + Example: internal.Tprintf(convertExample, map[string]interface{}{ + "appName": id.Name, + "command": "convert", + }), + Args: validateConvertArgs, + PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), + RunE: func(cmd *cobra.Command, args []string) error { + return RunConvert(opts, args[0]) + }, + }, opts) +} + +func validateConvertArgs(cmd *cobra.Command, args []string) error { + return validateArgs(cmd, args, "an SBOM argument is required") +} + +func RunConvert(opts *ConvertOptions, userInput string) error { + log.Warn("convert is an experimental feature, run `syft convert -h` for help") + + writer, err := opts.SBOMWriter() + if err != nil { + return err + } + + var reader io.ReadCloser + + if userInput == "-" { + reader = os.Stdin + } else { + f, err := os.Open(userInput) + if err != nil { + return fmt.Errorf("failed to open SBOM file: %w", err) + } + defer func() { + _ = f.Close() + }() + reader = f + } + + s, _, err := formats.Decode(reader) + if err != nil { + return fmt.Errorf("failed to decode SBOM: %w", err) + } + + if s == nil { + return fmt.Errorf("no SBOM produced") + } + + if err := writer.Write(*s); err != nil { + return fmt.Errorf("failed to write SBOM: %w", err) + } + + return nil +} diff --git a/cmd/syft/cli/commands/packages.go b/cmd/syft/cli/commands/packages.go new file mode 100644 index 000000000..c3a67992f --- /dev/null +++ b/cmd/syft/cli/commands/packages.go @@ -0,0 +1,253 @@ +package commands + +import ( + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/spf13/cobra" + + "github.com/anchore/clio" + "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/cmd/syft/cli/eventloop" + "github.com/anchore/syft/cmd/syft/cli/options" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/file" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/formats/template" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +const ( + packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages + {{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details + {{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM + {{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM + {{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM + {{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM + {{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM + {{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM + {{.appName}} {{.command}} alpine:latest -vv show verbose debug information + {{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file + + Supports the following image sources: + {{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry. + {{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory +` + + schemeHelpHeader = "You can also explicitly specify the scheme to use:" + imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon + {{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon + {{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) + {{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save" + {{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise) + {{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise) + {{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk +` + nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory) + {{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file) +` + packagesSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp + + packagesHelp = packagesExample + packagesSchemeHelp +) + +type packagesOptions struct { + options.Config `yaml:",inline" mapstructure:",squash"` + options.MultiOutput `yaml:",inline" mapstructure:",squash"` + options.UpdateCheck `yaml:",inline" mapstructure:",squash"` + options.Catalog `yaml:",inline" mapstructure:",squash"` +} + +func defaultPackagesOptions() *packagesOptions { + return &packagesOptions{ + MultiOutput: options.DefaultOutput(), + UpdateCheck: options.DefaultUpdateCheck(), + Catalog: options.DefaultCatalog(), + } +} + +//nolint:dupl +func Packages(app clio.Application) *cobra.Command { + id := app.ID() + + opts := defaultPackagesOptions() + + return app.SetupCommand(&cobra.Command{ + Use: "packages [SOURCE]", + Short: "Generate a package SBOM", + Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems", + Example: internal.Tprintf(packagesHelp, map[string]interface{}{ + "appName": id.Name, + "command": "packages", + }), + Args: validatePackagesArgs, + PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), + RunE: func(cmd *cobra.Command, args []string) error { + return runPackages(id, opts, args[0]) + }, + }, opts) +} + +func validatePackagesArgs(cmd *cobra.Command, args []string) error { + return validateArgs(cmd, args, "an image/directory argument is required") +} + +func validateArgs(cmd *cobra.Command, args []string, error string) error { + if len(args) == 0 { + // in the case that no arguments are given 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(error) + } + + return cobra.MaximumNArgs(1)(cmd, args) +} + +// nolint:funlen +func runPackages(id clio.Identification, opts *packagesOptions, userInput string) error { + err := validatePackageOutputOptions(&opts.MultiOutput) + if err != nil { + return err + } + + writer, err := opts.SBOMWriter() + if err != nil { + return err + } + + detection, err := source.Detect( + userInput, + source.DetectConfig{ + DefaultImageSource: opts.DefaultImagePullSource, + }, + ) + if err != nil { + return fmt.Errorf("could not deteremine source: %w", err) + } + + var platform *image.Platform + + if opts.Platform != "" { + platform, err = image.NewPlatform(opts.Platform) + if err != nil { + return fmt.Errorf("invalid platform: %w", err) + } + } + + hashers, err := file.Hashers(opts.Source.File.Digests...) + if err != nil { + return fmt.Errorf("invalid hash: %w", err) + } + + src, err := detection.NewSource( + source.DetectionSourceConfig{ + Alias: source.Alias{ + Name: opts.Source.Name, + Version: opts.Source.Version, + }, + RegistryOptions: opts.Registry.ToOptions(), + Platform: platform, + Exclude: source.ExcludeConfig{ + Paths: opts.Exclusions, + }, + DigestAlgorithms: hashers, + BasePath: opts.BasePath, + }, + ) + + if err != nil { + return fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) + } + + defer func() { + if src != nil { + if err := src.Close(); err != nil { + log.Tracef("unable to close source: %+v", err) + } + } + }() + + s, err := generateSBOM(id, src, &opts.Catalog) + if err != nil { + return err + } + + if s == nil { + return fmt.Errorf("no SBOM produced for %q", userInput) + } + + if err := writer.Write(*s); err != nil { + return fmt.Errorf("failed to write SBOM: %w", err) + } + + return nil +} + +func generateSBOM(id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) { + tasks, err := eventloop.Tasks(opts) + if err != nil { + return nil, err + } + + s := sbom.SBOM{ + Source: src.Describe(), + Descriptor: sbom.Descriptor{ + Name: id.Name, + Version: id.Version, + Configuration: opts, + }, + } + + err = buildRelationships(&s, src, tasks) + + return &s, err +} + +func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task) error { + var errs error + + var relationships []<-chan artifact.Relationship + for _, task := range tasks { + c := make(chan artifact.Relationship) + relationships = append(relationships, c) + go func(task eventloop.Task) { + err := eventloop.RunTask(task, &s.Artifacts, src, c) + if err != nil { + errs = multierror.Append(errs, err) + } + }(task) + } + + s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...) + + return errs +} + +func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) { + for _, c := range cs { + for n := range c { + relationships = append(relationships, n) + } + } + + return relationships +} + +func validatePackageOutputOptions(cfg *options.MultiOutput) error { + var usesTemplateOutput bool + for _, o := range cfg.Outputs { + if o == template.ID.String() { + usesTemplateOutput = true + break + } + } + + if usesTemplateOutput && cfg.OutputTemplatePath == "" { + return fmt.Errorf(`must specify path to template file when using "template" output format`) + } + + return nil +} diff --git a/cmd/syft/cli/commands/poweruser.go b/cmd/syft/cli/commands/poweruser.go new file mode 100644 index 000000000..b8776e56c --- /dev/null +++ b/cmd/syft/cli/commands/poweruser.go @@ -0,0 +1,154 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/gookit/color" + "github.com/hashicorp/go-multierror" + "github.com/spf13/cobra" + + "github.com/anchore/clio" + "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/cmd/syft/cli/eventloop" + "github.com/anchore/syft/cmd/syft/cli/options" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/formats/syftjson" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +const powerUserExample = ` {{.appName}} {{.command}} + DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0 + Template outputs are not supported. + All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration) +` + +type powerUserOptions struct { + options.Config `yaml:",inline" mapstructure:",squash"` + options.OutputFile `yaml:",inline" mapstructure:",squash"` + options.UpdateCheck `yaml:",inline" mapstructure:",squash"` + options.Catalog `yaml:",inline" mapstructure:",squash"` +} + +func PowerUser(app clio.Application) *cobra.Command { + id := app.ID() + + pkgs := options.DefaultCatalog() + pkgs.Secrets.Cataloger.Enabled = true + pkgs.FileMetadata.Cataloger.Enabled = true + pkgs.FileContents.Cataloger.Enabled = true + pkgs.FileClassification.Cataloger.Enabled = true + opts := &powerUserOptions{ + Catalog: pkgs, + } + + return app.SetupCommand(&cobra.Command{ + Use: "power-user [IMAGE]", + Short: "Run bulk operations on container images", + Example: internal.Tprintf(powerUserExample, map[string]interface{}{ + "appName": id.Name, + "command": "power-user", + }), + Args: validatePackagesArgs, + Hidden: true, + PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), + RunE: func(cmd *cobra.Command, args []string) error { + return runPowerUser(id, opts, args[0]) + }, + }, opts) +} + +//nolint:funlen +func runPowerUser(id clio.Identification, opts *powerUserOptions, userInput string) error { + writer, err := opts.SBOMWriter(syftjson.Format()) + if err != nil { + return err + } + defer func() { + // inform user at end of run that command will be removed + deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0") + fmt.Fprintln(os.Stderr, deprecated) + }() + + tasks, err := eventloop.Tasks(&opts.Catalog) + if err != nil { + return err + } + + detection, err := source.Detect( + userInput, + source.DetectConfig{ + DefaultImageSource: opts.DefaultImagePullSource, + }, + ) + if err != nil { + return fmt.Errorf("could not deteremine source: %w", err) + } + + var platform *image.Platform + + if opts.Platform != "" { + platform, err = image.NewPlatform(opts.Platform) + if err != nil { + return fmt.Errorf("invalid platform: %w", err) + } + } + + src, err := detection.NewSource( + source.DetectionSourceConfig{ + Alias: source.Alias{ + Name: opts.Source.Name, + Version: opts.Source.Version, + }, + RegistryOptions: opts.Registry.ToOptions(), + Platform: platform, + Exclude: source.ExcludeConfig{ + Paths: opts.Exclusions, + }, + DigestAlgorithms: nil, + BasePath: opts.BasePath, + }, + ) + + if src != nil { + defer src.Close() + } + if err != nil { + return fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) + } + + s := sbom.SBOM{ + Source: src.Describe(), + Descriptor: sbom.Descriptor{ + Name: id.Name, + Version: id.Version, + Configuration: opts, + }, + } + + var errs error + var relationships []<-chan artifact.Relationship + for _, task := range tasks { + c := make(chan artifact.Relationship) + relationships = append(relationships, c) + + go func(task eventloop.Task) { + err := eventloop.RunTask(task, &s.Artifacts, src, c) + errs = multierror.Append(errs, err) + }(task) + } + + if errs != nil { + return errs + } + + s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...) + + if err := writer.Write(s); err != nil { + return fmt.Errorf("failed to write sbom: %w", err) + } + + return nil +} diff --git a/cmd/syft/cli/commands/root.go b/cmd/syft/cli/commands/root.go new file mode 100644 index 000000000..fb3234b50 --- /dev/null +++ b/cmd/syft/cli/commands/root.go @@ -0,0 +1,27 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/anchore/clio" +) + +func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command { + id := app.ID() + + opts := defaultPackagesOptions() + + return app.SetupRootCommand(&cobra.Command{ + Use: fmt.Sprintf("%s [SOURCE]", app.ID().Name), + Short: packagesCmd.Short, + Long: packagesCmd.Long, + Args: packagesCmd.Args, + Example: packagesCmd.Example, + PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), + RunE: func(cmd *cobra.Command, args []string) error { + return runPackages(id, opts, args[0]) + }, + }, opts) +} diff --git a/cmd/syft/cli/commands/update.go b/cmd/syft/cli/commands/update.go new file mode 100644 index 000000000..88f159700 --- /dev/null +++ b/cmd/syft/cli/commands/update.go @@ -0,0 +1,121 @@ +package commands + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/spf13/cobra" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/clio" + hashiVersion "github.com/anchore/go-version" + "github.com/anchore/syft/cmd/syft/cli/options" + "github.com/anchore/syft/cmd/syft/internal" + "github.com/anchore/syft/internal/bus" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/event" + "github.com/anchore/syft/syft/event/parsers" +) + +var latestAppVersionURL = struct { + host string + path string +}{ + host: "https://toolbox-data.anchore.io", + path: "/syft/releases/latest/VERSION", +} + +func applicationUpdateCheck(id clio.Identification, check *options.UpdateCheck) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + if check.CheckForAppUpdate { + checkForApplicationUpdate(id) + } + return nil + } +} + +func checkForApplicationUpdate(id clio.Identification) { + log.Debugf("checking if a new version of %s is available", id.Name) + isAvailable, newVersion, err := isUpdateAvailable(id) + if err != nil { + // this should never stop the application + log.Errorf(err.Error()) + } + if isAvailable { + log.Infof("new version of %s is available: %s (current version is %s)", id.Name, newVersion, id.Version) + + bus.Publish(partybus.Event{ + Type: event.CLIAppUpdateAvailable, + Value: parsers.UpdateCheck{ + New: newVersion, + Current: id.Version, + }, + }) + } else { + log.Debugf("no new %s update available", id.Name) + } +} + +// isUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is. +func isUpdateAvailable(id clio.Identification) (bool, string, error) { + if !isProductionBuild(id.Version) { + // don't allow for non-production builds to check for a version. + return false, "", nil + } + + currentVersion, err := hashiVersion.NewVersion(id.Version) + if err != nil { + return false, "", fmt.Errorf("failed to parse current application version: %w", err) + } + + latestVersion, err := fetchLatestApplicationVersion(id) + if err != nil { + return false, "", err + } + + if latestVersion.GreaterThan(currentVersion) { + return true, latestVersion.String(), nil + } + + return false, "", nil +} + +func isProductionBuild(version string) bool { + if strings.Contains(version, "SNAPSHOT") || strings.Contains(version, internal.NotProvided) { + return false + } + return true +} + +func fetchLatestApplicationVersion(id clio.Identification) (*hashiVersion.Version, error) { + req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request for latest version: %w", err) + } + req.Header.Add("User-Agent", fmt.Sprintf("%v %v", id.Name, id.Version)) + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch latest version: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status) + } + + versionBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read latest version: %w", err) + } + + versionStr := strings.TrimSuffix(string(versionBytes), "\n") + if len(versionStr) > 50 { + return nil, fmt.Errorf("version too long: %q", versionStr[:50]) + } + + return hashiVersion.NewVersion(versionStr) +} diff --git a/internal/version/update_test.go b/cmd/syft/cli/commands/update_test.go similarity index 60% rename from internal/version/update_test.go rename to cmd/syft/cli/commands/update_test.go index c79750540..bc04a7465 100644 --- a/internal/version/update_test.go +++ b/cmd/syft/cli/commands/update_test.go @@ -1,11 +1,13 @@ -package version +package commands import ( "net/http" "net/http/httptest" "testing" + "github.com/anchore/clio" hashiVersion "github.com/anchore/go-version" + "github.com/anchore/syft/cmd/syft/internal" ) func TestIsUpdateAvailable(t *testing.T) { @@ -74,7 +76,7 @@ func TestIsUpdateAvailable(t *testing.T) { }, { name: "NoBuildVersion", - buildVersion: valueNotProvided, + buildVersion: internal.NotProvided, latestVersion: "1.0.0", code: 200, isAvailable: false, @@ -105,7 +107,7 @@ func TestIsUpdateAvailable(t *testing.T) { t.Run(test.name, func(t *testing.T) { // setup mocks // local... - version = test.buildVersion + id := clio.Identification{Name: "Syft", Version: test.buildVersion} // remote... handler := http.NewServeMux() handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) { @@ -116,7 +118,7 @@ func TestIsUpdateAvailable(t *testing.T) { latestAppVersionURL.host = mockSrv.URL defer mockSrv.Close() - isAvailable, newVersion, err := IsUpdateAvailable() + isAvailable, newVersion, err := isUpdateAvailable(id) if err != nil && !test.err { t.Fatalf("got error but expected none: %+v", err) } else if err == nil && test.err { @@ -137,52 +139,67 @@ func TestIsUpdateAvailable(t *testing.T) { func TestFetchLatestApplicationVersion(t *testing.T) { tests := []struct { - name string - response string - code int - err bool - expected *hashiVersion.Version + name string + response string + code int + err bool + id clio.Identification + expected *hashiVersion.Version + expectedHeaders map[string]string }{ { - name: "gocase", - response: "1.0.0", - code: 200, - expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")), + name: "gocase", + response: "1.0.0", + code: 200, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")), + expectedHeaders: map[string]string{"User-Agent": "Syft 0.0.0"}, + err: false, }, { - name: "garbage", - response: "garbage", - code: 200, - expected: nil, - err: true, + name: "garbage", + response: "garbage", + code: 200, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, { - name: "http 500", - response: "1.0.0", - code: 500, - expected: nil, - err: true, + name: "http 500", + response: "1.0.0", + code: 500, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, { - name: "http 404", - response: "1.0.0", - code: 404, - expected: nil, - err: true, + name: "http 404", + response: "1.0.0", + code: 404, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, { - name: "empty", - response: "", - code: 200, - expected: nil, - err: true, + name: "empty", + response: "", + code: 200, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, { - name: "too long", - response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ", - code: 200, - expected: nil, - err: true, + name: "too long", + response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ", + code: 200, + id: clio.Identification{Name: "Syft", Version: "0.0.0"}, + expected: nil, + expectedHeaders: nil, + err: true, }, } @@ -191,6 +208,15 @@ func TestFetchLatestApplicationVersion(t *testing.T) { // setup mock handler := http.NewServeMux() handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) { + if test.expectedHeaders != nil { + for headerName, headerValue := range test.expectedHeaders { + actualHeader := r.Header.Get(headerName) + if actualHeader != headerValue { + t.Fatalf("expected header %v=%v but got %v", headerName, headerValue, actualHeader) + } + } + } + w.WriteHeader(test.code) _, _ = w.Write([]byte(test.response)) }) @@ -198,7 +224,7 @@ func TestFetchLatestApplicationVersion(t *testing.T) { latestAppVersionURL.host = mockSrv.URL defer mockSrv.Close() - actual, err := fetchLatestApplicationVersion() + actual, err := fetchLatestApplicationVersion(test.id) if err != nil && !test.err { t.Fatalf("got error but expected none: %+v", err) } else if err == nil && test.err { diff --git a/cmd/syft/cli/convert.go b/cmd/syft/cli/convert.go deleted file mode 100644 index 16c24cac5..000000000 --- a/cmd/syft/cli/convert.go +++ /dev/null @@ -1,58 +0,0 @@ -package cli - -import ( - "fmt" - "log" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/anchore/syft/cmd/syft/cli/convert" - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/config" -) - -const ( - convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout - {{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json"" - {{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json -` -) - -//nolint:dupl -func Convert(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "convert [SOURCE-SBOM] -o [FORMAT]", - Short: "Convert between SBOM formats", - Long: "[Experimental] Convert SBOM files to, and from, SPDX, CycloneDX and Syft's format. For more info about data loss between formats see https://github.com/anchore/syft#format-conversion-experimental", - Example: internal.Tprintf(convertExample, map[string]interface{}{ - "appName": internal.ApplicationName, - "command": "convert", - }), - Args: func(cmd *cobra.Command, args []string) error { - if err := app.LoadAllValues(v, ro.Config); err != nil { - return fmt.Errorf("invalid application config: %w", err) - } - newLogWrapper(app) - logApplicationConfig(app) - return validateArgs(cmd, args) - }, - SilenceUsage: true, - SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { - if app.CheckForAppUpdate { - checkForApplicationUpdate() - // TODO: this is broke, the bus isn't available yet - } - return convert.Run(cmd.Context(), app, args) - }, - } - - err := po.AddFlags(cmd, v) - if err != nil { - log.Fatal(err) - } - - return cmd -} diff --git a/cmd/syft/cli/convert/convert.go b/cmd/syft/cli/convert/convert.go deleted file mode 100644 index 2f0dbcedb..000000000 --- a/cmd/syft/cli/convert/convert.go +++ /dev/null @@ -1,85 +0,0 @@ -package convert - -import ( - "context" - "fmt" - "io" - "os" - - "github.com/wagoodman/go-partybus" - - "github.com/anchore/stereoscope" - "github.com/anchore/syft/cmd/syft/cli/eventloop" - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/cmd/syft/internal/ui" - "github.com/anchore/syft/internal/bus" - "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/formats" - "github.com/anchore/syft/syft/sbom" -) - -func Run(_ context.Context, app *config.Application, args []string) error { - log.Warn("convert is an experimental feature, run `syft convert -h` for help") - - writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath) - if err != nil { - return err - } - - // could be an image or a directory, with or without a scheme - userInput := args[0] - - var reader io.ReadCloser - - if userInput == "-" { - reader = os.Stdin - } else { - f, err := os.Open(userInput) - if err != nil { - return fmt.Errorf("failed to open SBOM file: %w", err) - } - defer func() { - _ = f.Close() - }() - reader = f - } - - eventBus := partybus.NewBus() - stereoscope.SetBus(eventBus) - syft.SetBus(eventBus) - subscription := eventBus.Subscribe() - - return eventloop.EventLoop( - execWorker(reader, writer), - eventloop.SetupSignals(), - subscription, - stereoscope.Cleanup, - ui.Select(options.IsVerbose(app), app.Quiet)..., - ) -} - -func execWorker(reader io.Reader, writer sbom.Writer) <-chan error { - errs := make(chan error) - go func() { - defer close(errs) - defer bus.Exit() - - s, _, err := formats.Decode(reader) - if err != nil { - errs <- fmt.Errorf("failed to decode SBOM: %w", err) - return - } - - if s == nil { - errs <- fmt.Errorf("no SBOM produced") - return - } - - if err := writer.Write(*s); err != nil { - errs <- fmt.Errorf("failed to write SBOM: %w", err) - } - }() - return errs -} diff --git a/cmd/syft/cli/eventloop/event_loop.go b/cmd/syft/cli/eventloop/event_loop.go deleted file mode 100644 index e7d008e71..000000000 --- a/cmd/syft/cli/eventloop/event_loop.go +++ /dev/null @@ -1,98 +0,0 @@ -package eventloop - -import ( - "errors" - "fmt" - "os" - - "github.com/hashicorp/go-multierror" - "github.com/wagoodman/go-partybus" - - "github.com/anchore/clio" - "github.com/anchore/syft/internal/log" -) - -// EventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and -// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until -// an eventual graceful exit. -func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...clio.UI) error { - defer cleanupFn() - events := subscription.Events() - var err error - var ux clio.UI - - if ux, err = setupUI(subscription, uxs...); err != nil { - return err - } - - var retErr error - var forceTeardown bool - - for { - if workerErrs == nil && events == nil { - break - } - select { - case err, isOpen := <-workerErrs: - if !isOpen { - workerErrs = nil - continue - } - if err != nil { - // capture the error from the worker and unsubscribe to complete a graceful shutdown - retErr = multierror.Append(retErr, err) - _ = subscription.Unsubscribe() - // the worker has exited, we may have been mid-handling events for the UI which should now be - // ignored, in which case forcing a teardown of the UI irregardless of the state is required. - forceTeardown = true - } - case e, isOpen := <-events: - if !isOpen { - events = nil - continue - } - - if err := ux.Handle(e); err != nil { - if errors.Is(err, partybus.ErrUnsubscribe) { - events = nil - } else { - retErr = multierror.Append(retErr, err) - // TODO: should we unsubscribe? should we try to halt execution? or continue? - } - } - case <-signals: - // ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up. - // we ignore further errors since cleaning up the tmp directories will affect running catalogers that are - // reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result. - - // TODO: potential future improvement would be to pass context into workers with a cancel function that is - // to the event loop. In this way we can have a more controlled shutdown even at the most nested levels - // of processing. - events = nil - workerErrs = nil - forceTeardown = true - } - } - - if err := ux.Teardown(forceTeardown); err != nil { - retErr = multierror.Append(retErr, err) - } - - return retErr -} - -// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use -// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error -// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks -// when there are environmental problem (e.g. unable to setup a TUI with the current TTY). -func setupUI(subscription *partybus.Subscription, uis ...clio.UI) (clio.UI, error) { - for _, ux := range uis { - if err := ux.Setup(subscription); err != nil { - log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err) - continue - } - - return ux, nil - } - return nil, fmt.Errorf("unable to setup any UI") -} diff --git a/cmd/syft/cli/eventloop/event_loop_test.go b/cmd/syft/cli/eventloop/event_loop_test.go deleted file mode 100644 index 495af90b8..000000000 --- a/cmd/syft/cli/eventloop/event_loop_test.go +++ /dev/null @@ -1,459 +0,0 @@ -package eventloop - -import ( - "fmt" - "os" - "syscall" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/wagoodman/go-partybus" - - "github.com/anchore/clio" - "github.com/anchore/syft/syft/event" -) - -var _ clio.UI = (*uiMock)(nil) - -type uiMock struct { - t *testing.T - finalEvent partybus.Event - subscription partybus.Unsubscribable - mock.Mock -} - -func (u *uiMock) Setup(unsubscribe partybus.Unsubscribable) error { - u.t.Helper() - u.t.Logf("UI Setup called") - u.subscription = unsubscribe - return u.Called(unsubscribe.Unsubscribe).Error(0) -} - -func (u *uiMock) Handle(event partybus.Event) error { - u.t.Helper() - u.t.Logf("UI Handle called: %+v", event.Type) - if event == u.finalEvent { - assert.NoError(u.t, u.subscription.Unsubscribe()) - } - return u.Called(event).Error(0) -} - -func (u *uiMock) Teardown(_ bool) error { - u.t.Helper() - u.t.Logf("UI Teardown called") - return u.Called().Error(0) -} - -func Test_EventLoop_gracefulExit(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - finalEvent := partybus.Event{ - Type: event.CLIExit, - } - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - close(ret) - t.Log("worker closed") - // do the other half of the shutdown - testBus.Publish(finalEvent) - t.Log("worker published final event") - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - finalEvent: finalEvent, - } - - // ensure the mock sees at least the final event - ux.On("Handle", finalEvent).Return(nil) - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - assert.NoError(t, - EventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_EventLoop_workerError(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - workerErr := fmt.Errorf("worker error") - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - ret <- workerErr - t.Log("worker sent error") - close(ret) - t.Log("worker closed") - // note: NO final event is fired - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - } - - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - // ensure we see an error returned - assert.ErrorIs(t, - EventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - workerErr, - "should have seen a worker error, but did not", - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_EventLoop_unsubscribeError(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - finalEvent := partybus.Event{ - Type: event.CLIExit, - } - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - close(ret) - t.Log("worker closed") - // do the other half of the shutdown - testBus.Publish(finalEvent) - t.Log("worker published final event") - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - finalEvent: finalEvent, - } - - // ensure the mock sees at least the final event... note the unsubscribe error here - ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe) - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - // unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that - // this case is handled as a controlled shutdown (this test should not timeout) - assert.NoError(t, - EventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_EventLoop_handlerError(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - finalEvent := partybus.Event{ - Type: event.CLIExit, - Error: fmt.Errorf("an exit error occured"), - } - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - close(ret) - t.Log("worker closed") - // do the other half of the shutdown - testBus.Publish(finalEvent) - t.Log("worker published final event") - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - finalEvent: finalEvent, - } - - // ensure the mock sees at least the final event... note the event error is propagated - ux.On("Handle", finalEvent).Return(finalEvent.Error) - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - // handle errors SHOULD propagate the event loop. We are additionally asserting that this case is - // handled as a controlled shutdown (this test should not timeout) - assert.ErrorIs(t, - EventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - finalEvent.Error, - "should have seen a event error, but did not", - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_EventLoop_signalsStopExecution(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - worker := func() <-chan error { - // the worker will never return work and the event loop will always be waiting... - return make(chan error) - } - - signaler := func() <-chan os.Signal { - ret := make(chan os.Signal) - go func() { - ret <- syscall.SIGINT - // note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit - }() - return ret - } - - ux := &uiMock{ - t: t, - } - - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - assert.NoError(t, - EventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_EventLoop_uiTeardownError(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - finalEvent := partybus.Event{ - Type: event.CLIExit, - } - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - close(ret) - t.Log("worker closed") - // do the other half of the shutdown - testBus.Publish(finalEvent) - t.Log("worker published final event") - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - finalEvent: finalEvent, - } - - teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down") - - // ensure the mock sees at least the final event... note the event error is propagated - ux.On("Handle", finalEvent).Return(nil) - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(teardownError) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - // ensure we see an error returned - assert.ErrorIs(t, - EventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - teardownError, - "should have seen a UI teardown error, but did not", - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) { - done := make(chan bool) - go func() { - test(t) - done <- true - }() - - select { - case <-time.After(timeout): - t.Fatal("test timed out") - case <-done: - } -} diff --git a/cmd/syft/cli/eventloop/signals.go b/cmd/syft/cli/eventloop/signals.go deleted file mode 100644 index 72a97711e..000000000 --- a/cmd/syft/cli/eventloop/signals.go +++ /dev/null @@ -1,20 +0,0 @@ -package eventloop - -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/syft/cli/eventloop/tasks.go b/cmd/syft/cli/eventloop/tasks.go index b6121d0da..feabca573 100644 --- a/cmd/syft/cli/eventloop/tasks.go +++ b/cmd/syft/cli/eventloop/tasks.go @@ -1,7 +1,7 @@ package eventloop import ( - "github.com/anchore/syft/internal/config" + "github.com/anchore/syft/cmd/syft/cli/options" "github.com/anchore/syft/internal/file" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/artifact" @@ -15,10 +15,10 @@ import ( type Task func(*sbom.Artifacts, source.Source) ([]artifact.Relationship, error) -func Tasks(app *config.Application) ([]Task, error) { +func Tasks(opts *options.Catalog) ([]Task, error) { var tasks []Task - generators := []func(app *config.Application) (Task, error){ + generators := []func(opts *options.Catalog) (Task, error){ generateCatalogPackagesTask, generateCatalogFileMetadataTask, generateCatalogFileDigestsTask, @@ -27,7 +27,7 @@ func Tasks(app *config.Application) ([]Task, error) { } for _, generator := range generators { - task, err := generator(app) + task, err := generator(opts) if err != nil { return nil, err } @@ -40,13 +40,13 @@ func Tasks(app *config.Application) ([]Task, error) { return tasks, nil } -func generateCatalogPackagesTask(app *config.Application) (Task, error) { - if !app.Package.Cataloger.Enabled { +func generateCatalogPackagesTask(opts *options.Catalog) (Task, error) { + if !opts.Package.Cataloger.Enabled { return nil, nil } task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) { - packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, app.ToCatalogerConfig()) + packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, opts.ToCatalogerConfig()) results.Packages = packageCatalog results.LinuxDistribution = theDistro @@ -57,15 +57,15 @@ func generateCatalogPackagesTask(app *config.Application) (Task, error) { return task, nil } -func generateCatalogFileMetadataTask(app *config.Application) (Task, error) { - if !app.FileMetadata.Cataloger.Enabled { +func generateCatalogFileMetadataTask(opts *options.Catalog) (Task, error) { + if !opts.FileMetadata.Cataloger.Enabled { return nil, nil } metadataCataloger := filemetadata.NewCataloger() task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) { - resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt) + resolver, err := src.FileResolver(opts.FileMetadata.Cataloger.GetScope()) if err != nil { return nil, err } @@ -81,12 +81,12 @@ func generateCatalogFileMetadataTask(app *config.Application) (Task, error) { return task, nil } -func generateCatalogFileDigestsTask(app *config.Application) (Task, error) { - if !app.FileMetadata.Cataloger.Enabled { +func generateCatalogFileDigestsTask(opts *options.Catalog) (Task, error) { + if !opts.FileMetadata.Cataloger.Enabled { return nil, nil } - hashes, err := file.Hashers(app.FileMetadata.Digests...) + hashes, err := file.Hashers(opts.FileMetadata.Digests...) if err != nil { return nil, err } @@ -94,7 +94,7 @@ func generateCatalogFileDigestsTask(app *config.Application) (Task, error) { digestsCataloger := filedigest.NewCataloger(hashes) task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) { - resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt) + resolver, err := src.FileResolver(opts.FileMetadata.Cataloger.GetScope()) if err != nil { return nil, err } @@ -110,23 +110,23 @@ func generateCatalogFileDigestsTask(app *config.Application) (Task, error) { return task, nil } -func generateCatalogSecretsTask(app *config.Application) (Task, error) { - if !app.Secrets.Cataloger.Enabled { +func generateCatalogSecretsTask(opts *options.Catalog) (Task, error) { + if !opts.Secrets.Cataloger.Enabled { return nil, nil } - patterns, err := secrets.GenerateSearchPatterns(secrets.DefaultSecretsPatterns, app.Secrets.AdditionalPatterns, app.Secrets.ExcludePatternNames) + patterns, err := secrets.GenerateSearchPatterns(secrets.DefaultSecretsPatterns, opts.Secrets.AdditionalPatterns, opts.Secrets.ExcludePatternNames) if err != nil { return nil, err } - secretsCataloger, err := secrets.NewCataloger(patterns, app.Secrets.RevealValues, app.Secrets.SkipFilesAboveSize) //nolint:staticcheck + secretsCataloger, err := secrets.NewCataloger(patterns, opts.Secrets.RevealValues, opts.Secrets.SkipFilesAboveSize) //nolint:staticcheck if err != nil { return nil, err } task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) { - resolver, err := src.FileResolver(app.Secrets.Cataloger.ScopeOpt) + resolver, err := src.FileResolver(opts.Secrets.Cataloger.GetScope()) if err != nil { return nil, err } @@ -142,18 +142,18 @@ func generateCatalogSecretsTask(app *config.Application) (Task, error) { return task, nil } -func generateCatalogContentsTask(app *config.Application) (Task, error) { - if !app.FileContents.Cataloger.Enabled { +func generateCatalogContentsTask(opts *options.Catalog) (Task, error) { + if !opts.FileContents.Cataloger.Enabled { return nil, nil } - contentsCataloger, err := filecontent.NewCataloger(app.FileContents.Globs, app.FileContents.SkipFilesAboveSize) //nolint:staticcheck + contentsCataloger, err := filecontent.NewCataloger(opts.FileContents.Globs, opts.FileContents.SkipFilesAboveSize) //nolint:staticcheck if err != nil { return nil, err } task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) { - resolver, err := src.FileResolver(app.FileContents.Cataloger.ScopeOpt) + resolver, err := src.FileResolver(opts.FileContents.Cataloger.GetScope()) if err != nil { return nil, err } @@ -169,16 +169,17 @@ func generateCatalogContentsTask(app *config.Application) (Task, error) { return task, nil } -func RunTask(t Task, a *sbom.Artifacts, src source.Source, c chan<- artifact.Relationship, errs chan<- error) { +func RunTask(t Task, a *sbom.Artifacts, src source.Source, c chan<- artifact.Relationship) error { defer close(c) relationships, err := t(a, src) if err != nil { - errs <- err - return + return err } for _, relationship := range relationships { c <- relationship } + + return nil } diff --git a/cmd/syft/cli/options/attest.go b/cmd/syft/cli/options/attest.go index 60d72191a..8c0420386 100644 --- a/cmd/syft/cli/options/attest.go +++ b/cmd/syft/cli/options/attest.go @@ -1,26 +1,17 @@ package options import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" + "github.com/anchore/clio" ) -type AttestOptions struct { - Key string +type Attest struct { + // IMPORTANT: do not show the attestation key/password in any YAML/JSON output (sensitive information) + Key secret `yaml:"key" json:"key" mapstructure:"key"` + Password secret `yaml:"password" json:"password" mapstructure:"password"` } -var _ Interface = (*AttestOptions)(nil) +var _ clio.FlagAdder = (*Attest)(nil) -func (o AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error { - cmd.Flags().StringVarP(&o.Key, "key", "k", "", "the key to use for the attestation") - return bindAttestConfigOptions(cmd.Flags(), v) -} - -//nolint:revive -func bindAttestConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error { - if err := v.BindPFlag("attest.key", flags.Lookup("key")); err != nil { - return err - } - return nil +func (o Attest) AddFlags(flags clio.FlagSet) { + flags.StringVarP((*string)(&o.Key), "key", "k", "the key to use for the attestation") } diff --git a/cmd/syft/cli/options/catalog.go b/cmd/syft/cli/options/catalog.go new file mode 100644 index 000000000..a9abf8760 --- /dev/null +++ b/cmd/syft/cli/options/catalog.go @@ -0,0 +1,168 @@ +package options + +import ( + "fmt" + "sort" + "strings" + + "github.com/iancoleman/strcase" + "github.com/mitchellh/go-homedir" + + "github.com/anchore/clio" + "github.com/anchore/fangs" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg/cataloger" + golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang" + "github.com/anchore/syft/syft/pkg/cataloger/kernel" + pythonCataloger "github.com/anchore/syft/syft/pkg/cataloger/python" + "github.com/anchore/syft/syft/source" +) + +type Catalog struct { + Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"` + Package pkg `yaml:"package" json:"package" mapstructure:"package"` + Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"` + LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"` + Python python `yaml:"python" json:"python" mapstructure:"python"` + FileMetadata fileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"` + FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"` + FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"` + Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"` + Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` + Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` + Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` + Name string `yaml:"name" json:"name" mapstructure:"name"` + Source sourceCfg `yaml:"source" json:"source" mapstructure:"source"` + Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel + DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source + BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths + ExcludeBinaryOverlapByOwnership bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"` // exclude synthetic binary packages owned by os package files +} + +var _ interface { + clio.FlagAdder + clio.PostLoader +} = (*Catalog)(nil) + +func DefaultCatalog() Catalog { + return Catalog{ + Package: defaultPkg(), + LinuxKernel: defaultLinuxKernel(), + FileMetadata: defaultFileMetadata(), + FileClassification: defaultFileClassification(), + FileContents: defaultFileContents(), + Secrets: defaultSecrets(), + Source: defaultSourceCfg(), + Parallelism: 1, + ExcludeBinaryOverlapByOwnership: true, + } +} + +func (cfg *Catalog) AddFlags(flags clio.FlagSet) { + var validScopeValues []string + for _, scope := range source.AllScopes { + validScopeValues = append(validScopeValues, strcase.ToDelimited(string(scope), '-')) + } + flags.StringVarP(&cfg.Package.Cataloger.Scope, "scope", "s", + fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues)) + + flags.StringVarP(&cfg.Platform, "platform", "", + "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')") + + flags.StringArrayVarP(&cfg.Exclusions, "exclude", "", + "exclude paths from being scanned using a glob expression") + + flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "", + "enable one or more package catalogers") + + flags.StringVarP(&cfg.Source.Name, "name", "", + "set the name of the target being analyzed") + + if pfp, ok := flags.(fangs.PFlagSetProvider); ok { + flagSet := pfp.PFlagSet() + flagSet.Lookup("name").Deprecated = "use: source-name" + } + + flags.StringVarP(&cfg.Source.Name, "source-name", "", + "set the name of the target being analyzed") + + flags.StringVarP(&cfg.Source.Version, "source-version", "", + "set the version of the target being analyzed") + + flags.StringVarP(&cfg.BasePath, "base-path", "", + "base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory") +} + +func (cfg *Catalog) PostLoad() error { + // parse options on this struct + var catalogers []string + for _, c := range cfg.Catalogers { + for _, f := range strings.Split(c, ",") { + catalogers = append(catalogers, strings.TrimSpace(f)) + } + } + sort.Strings(catalogers) + cfg.Catalogers = catalogers + + if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil { + return err + } + + if cfg.Name != "" { + log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version") + if cfg.Source.Name == "" { + cfg.Source.Name = cfg.Name + } + } + + return nil +} + +func (cfg Catalog) ToCatalogerConfig() cataloger.Config { + return cataloger.Config{ + Search: cataloger.SearchConfig{ + IncludeIndexedArchives: cfg.Package.SearchIndexedArchives, + IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives, + Scope: cfg.Package.Cataloger.GetScope(), + }, + Catalogers: cfg.Catalogers, + Parallelism: cfg.Parallelism, + Golang: golangCataloger.NewGoCatalogerOpts(). + WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses). + WithLocalModCacheDir(cfg.Golang.LocalModCacheDir). + WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses). + WithProxy(cfg.Golang.Proxy). + WithNoProxy(cfg.Golang.NoProxy), + LinuxKernel: kernel.LinuxCatalogerConfig{ + CatalogModules: cfg.LinuxKernel.CatalogModules, + }, + Python: pythonCataloger.CatalogerConfig{ + GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements, + }, + ExcludeBinaryOverlapByOwnership: cfg.ExcludeBinaryOverlapByOwnership, + } +} + +var validDefaultSourceValues = []string{"registry", "docker", "podman", ""} + +func checkDefaultSourceValues(source string) error { + validValues := internal.NewStringSet(validDefaultSourceValues...) + if !validValues.Contains(source) { + validValuesString := strings.Join(validDefaultSourceValues, ", ") + return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString) + } + + return nil +} + +func expandFilePath(file string) (string, error) { + if file != "" { + expandedPath, err := homedir.Expand(file) + if err != nil { + return "", fmt.Errorf("unable to expand file path=%q: %w", file, err) + } + file = expandedPath + } + return file, nil +} diff --git a/cmd/syft/cli/options/config.go b/cmd/syft/cli/options/config.go new file mode 100644 index 000000000..85aeb69dd --- /dev/null +++ b/cmd/syft/cli/options/config.go @@ -0,0 +1,6 @@ +package options + +// Config holds a reference to the specific config file that was used to load application configuration +type Config struct { + ConfigFile string `yaml:"config" json:"config" mapstructure:"config"` +} diff --git a/cmd/syft/cli/options/file_classification.go b/cmd/syft/cli/options/file_classification.go new file mode 100644 index 000000000..9f1abcdfb --- /dev/null +++ b/cmd/syft/cli/options/file_classification.go @@ -0,0 +1,17 @@ +package options + +import ( + "github.com/anchore/syft/syft/source" +) + +type fileClassification struct { + Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` +} + +func defaultFileClassification() fileClassification { + return fileClassification{ + Cataloger: scope{ + Scope: source.SquashedScope.String(), + }, + } +} diff --git a/cmd/syft/cli/options/file_contents.go b/cmd/syft/cli/options/file_contents.go new file mode 100644 index 000000000..6dba465f5 --- /dev/null +++ b/cmd/syft/cli/options/file_contents.go @@ -0,0 +1,21 @@ +package options + +import ( + "github.com/anchore/syft/internal/file" + "github.com/anchore/syft/syft/source" +) + +type fileContents struct { + Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` + SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"` + Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"` +} + +func defaultFileContents() fileContents { + return fileContents{ + Cataloger: scope{ + Scope: source.SquashedScope.String(), + }, + SkipFilesAboveSize: 1 * file.MB, + } +} diff --git a/cmd/syft/cli/options/file_metadata.go b/cmd/syft/cli/options/file_metadata.go new file mode 100644 index 000000000..eb2335a24 --- /dev/null +++ b/cmd/syft/cli/options/file_metadata.go @@ -0,0 +1,19 @@ +package options + +import ( + "github.com/anchore/syft/syft/source" +) + +type fileMetadata struct { + Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` + Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"` +} + +func defaultFileMetadata() fileMetadata { + return fileMetadata{ + Cataloger: scope{ + Scope: source.SquashedScope.String(), + }, + Digests: []string{"sha256"}, + } +} diff --git a/cmd/syft/cli/options/fulcio.go b/cmd/syft/cli/options/fulcio.go deleted file mode 100644 index b1dec9eb6..000000000 --- a/cmd/syft/cli/options/fulcio.go +++ /dev/null @@ -1,49 +0,0 @@ -package options - -import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -const defaultFulcioURL = "https://fulcio.sigstore.dev" - -// FulcioOptions is the wrapper for Fulcio related options. -type FulcioOptions struct { - URL string - IdentityToken string - InsecureSkipFulcioVerify bool -} - -var _ Interface = (*FulcioOptions)(nil) - -// AddFlags implements Interface -func (o *FulcioOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error { - // TODO: change this back to api.SigstorePublicServerURL after the v1 migration is complete. - cmd.Flags().StringVar(&o.URL, "fulcio-url", defaultFulcioURL, - "address of sigstore PKI server") - - cmd.Flags().StringVar(&o.IdentityToken, "identity-token", "", - "identity token to use for certificate from fulcio") - - cmd.Flags().BoolVar(&o.InsecureSkipFulcioVerify, "insecure-skip-verify", false, - "skip verifying fulcio certificat and the SCT (Signed Certificate Timestamp) (this should only be used for testing).") - return bindFulcioConfigOptions(cmd.Flags(), v) -} - -//nolint:revive -func bindFulcioConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error { - if err := v.BindPFlag("attest.fulcio-url", flags.Lookup("fulcio-url")); err != nil { - return err - } - - if err := v.BindPFlag("attest.fulcio-identity-token", flags.Lookup("identity-token")); err != nil { - return err - } - - if err := v.BindPFlag("attest.insecure-skip-verify", flags.Lookup("insecure-skip-verify")); err != nil { - return err - } - - return nil -} diff --git a/internal/config/golang.go b/cmd/syft/cli/options/golang.go similarity index 64% rename from internal/config/golang.go rename to cmd/syft/cli/options/golang.go index 56ebbed08..ff99f414f 100644 --- a/internal/config/golang.go +++ b/cmd/syft/cli/options/golang.go @@ -1,6 +1,4 @@ -package config - -import "github.com/spf13/viper" +package options type golang struct { SearchLocalModCacheLicenses bool `json:"search-local-mod-cache-licenses" yaml:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"` @@ -9,11 +7,3 @@ type golang struct { Proxy string `json:"proxy" yaml:"proxy" mapstructure:"proxy"` NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"` } - -func (cfg golang) loadDefaultValues(v *viper.Viper) { - v.SetDefault("golang.search-local-mod-cache-licenses", false) - v.SetDefault("golang.local-mod-cache-dir", "") - v.SetDefault("golang.search-remote-licenses", false) - v.SetDefault("golang.proxy", "") - v.SetDefault("golang.no-proxy", "") -} diff --git a/cmd/syft/cli/options/linux_kernel.go b/cmd/syft/cli/options/linux_kernel.go new file mode 100644 index 000000000..c56466abf --- /dev/null +++ b/cmd/syft/cli/options/linux_kernel.go @@ -0,0 +1,11 @@ +package options + +type linuxKernel struct { + CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"` +} + +func defaultLinuxKernel() linuxKernel { + return linuxKernel{ + CatalogModules: true, + } +} diff --git a/cmd/syft/cli/options/oidc.go b/cmd/syft/cli/options/oidc.go deleted file mode 100644 index 580ecc176..000000000 --- a/cmd/syft/cli/options/oidc.go +++ /dev/null @@ -1,49 +0,0 @@ -package options - -import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -const DefaultOIDCIssuerURL = "https://oauth2.sigstore.dev/auth" - -// OIDCOptions is the wrapper for OIDC related options. -type OIDCOptions struct { - Issuer string - ClientID string - RedirectURL string -} - -var _ Interface = (*OIDCOptions)(nil) - -// AddFlags implements Interface -func (o *OIDCOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error { - cmd.Flags().StringVar(&o.Issuer, "oidc-issuer", DefaultOIDCIssuerURL, - "OIDC provider to be used to issue ID token") - - cmd.Flags().StringVar(&o.ClientID, "oidc-client-id", "sigstore", - "OIDC client ID for application") - - cmd.Flags().StringVar(&o.RedirectURL, "oidc-redirect-url", "", - "OIDC redirect URL (Optional)") - - return bindOIDCConfigOptions(cmd.Flags(), v) -} - -//nolint:revive -func bindOIDCConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error { - if err := v.BindPFlag("attest.oidc-issuer", flags.Lookup("oidc-issuer")); err != nil { - return err - } - - if err := v.BindPFlag("attest.oidc-client-id", flags.Lookup("oidc-client-id")); err != nil { - return err - } - - if err := v.BindPFlag("attest.oidc-redirect-url", flags.Lookup("oidc-redirect-url")); err != nil { - return err - } - - return nil -} diff --git a/cmd/syft/cli/options/options.go b/cmd/syft/cli/options/options.go deleted file mode 100644 index f8646bbb9..000000000 --- a/cmd/syft/cli/options/options.go +++ /dev/null @@ -1,11 +0,0 @@ -package options - -import ( - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -type Interface interface { - // AddFlags adds this options' flags to the cobra command. - AddFlags(cmd *cobra.Command, v *viper.Viper) error -} diff --git a/cmd/syft/cli/options/output.go b/cmd/syft/cli/options/output.go new file mode 100644 index 000000000..09c226f3e --- /dev/null +++ b/cmd/syft/cli/options/output.go @@ -0,0 +1,100 @@ +package options + +import ( + "fmt" + "slices" + + "github.com/anchore/clio" + "github.com/anchore/fangs" + "github.com/anchore/syft/syft/formats" + "github.com/anchore/syft/syft/formats/table" + "github.com/anchore/syft/syft/formats/template" + "github.com/anchore/syft/syft/sbom" +) + +// MultiOutput has the standard output options syft accepts: multiple -o, --file, --template +type MultiOutput struct { + Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output + OutputFile `yaml:",inline" json:"" mapstructure:",squash"` + OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output +} + +var _ interface { + clio.FlagAdder +} = (*MultiOutput)(nil) + +func DefaultOutput() MultiOutput { + return MultiOutput{ + Outputs: []string{string(table.ID)}, + } +} + +func (o *MultiOutput) AddFlags(flags clio.FlagSet) { + flags.StringArrayVarP(&o.Outputs, "output", "o", + fmt.Sprintf("report output format (= to output to a file), formats=%v", formats.AllIDs())) + + flags.StringVarP(&o.OutputTemplatePath, "template", "t", + "specify the path to a Go template file") +} + +func (o *MultiOutput) SBOMWriter() (sbom.Writer, error) { + return makeSBOMWriter(o.Outputs, o.File, o.OutputTemplatePath) +} + +// SingleOutput allows only 1 output to be specified, with a user able to set what options are allowed by setting AllowableOptions +type SingleOutput struct { + AllowableOptions []string `yaml:"-" json:"-" mapstructure:"-"` + Output string `yaml:"output" json:"output" mapstructure:"output"` + OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output +} + +var _ clio.FlagAdder = (*SingleOutput)(nil) + +func (o *SingleOutput) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&o.Output, "output", "o", + fmt.Sprintf("report output format, options=%v", o.AllowableOptions)) + + if slices.Contains(o.AllowableOptions, template.ID.String()) { + flags.StringVarP(&o.OutputTemplatePath, "template", "t", + "specify the path to a Go template file") + } +} + +func (o *SingleOutput) SBOMWriter(file string) (sbom.Writer, error) { + return makeSBOMWriter([]string{o.Output}, file, o.OutputTemplatePath) +} + +// Deprecated: OutputFile is only the --file argument +type OutputFile struct { + File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to +} + +var _ interface { + clio.FlagAdder + clio.PostLoader +} = (*OutputFile)(nil) + +func (o *OutputFile) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&o.File, "file", "", + "file to write the default report output to (default is STDOUT)") + + if pfp, ok := flags.(fangs.PFlagSetProvider); ok { + flagSet := pfp.PFlagSet() + flagSet.Lookup("file").Deprecated = "use: output" + } +} + +func (o *OutputFile) PostLoad() error { + if o.File != "" { + file, err := expandFilePath(o.File) + if err != nil { + return err + } + o.File = file + } + return nil +} + +func (o *OutputFile) SBOMWriter(f sbom.Format) (sbom.Writer, error) { + return makeSBOMWriterForFormat(f, o.File) +} diff --git a/cmd/syft/cli/options/packages.go b/cmd/syft/cli/options/packages.go deleted file mode 100644 index 259a78781..000000000 --- a/cmd/syft/cli/options/packages.go +++ /dev/null @@ -1,118 +0,0 @@ -package options - -import ( - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" - - "github.com/anchore/syft/syft/formats" - "github.com/anchore/syft/syft/formats/table" - "github.com/anchore/syft/syft/pkg/cataloger" - "github.com/anchore/syft/syft/source" -) - -type PackagesOptions struct { - Scope string - Output []string - OutputTemplatePath string - File string - Platform string - Exclude []string - Catalogers []string - SourceName string - SourceVersion string - BasePath string -} - -var _ Interface = (*PackagesOptions)(nil) - -func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error { - cmd.Flags().StringVarP(&o.Scope, "scope", "s", cataloger.DefaultSearchConfig().Scope.String(), - fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes)) - - cmd.Flags().StringArrayVarP(&o.Output, "output", "o", []string{string(table.ID)}, - fmt.Sprintf("report output format, options=%v", formats.AllIDs())) - - cmd.Flags().StringVarP(&o.File, "file", "", "", - "file to write the default report output to (default is STDOUT)") - - cmd.Flags().StringVarP(&o.OutputTemplatePath, "template", "t", "", - "specify the path to a Go template file") - - cmd.Flags().StringVarP(&o.Platform, "platform", "", "", - "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')") - - cmd.Flags().StringArrayVarP(&o.Exclude, "exclude", "", nil, - "exclude paths from being scanned using a glob expression") - - cmd.Flags().StringArrayVarP(&o.Catalogers, "catalogers", "", nil, - "enable one or more package catalogers") - - cmd.Flags().StringVarP(&o.SourceName, "name", "", "", - "set the name of the target being analyzed") - cmd.Flags().Lookup("name").Deprecated = "use: source-name" - - cmd.Flags().StringVarP(&o.SourceName, "source-name", "", "", - "set the name of the target being analyzed") - - cmd.Flags().StringVarP(&o.SourceVersion, "source-version", "", "", - "set the name of the target being analyzed") - - cmd.Flags().StringVarP(&o.BasePath, "base-path", "", "", - "base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory") - - return bindPackageConfigOptions(cmd.Flags(), v) -} - -//nolint:revive -func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error { - // Formatting & Input options ////////////////////////////////////////////// - - if err := v.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil { - return err - } - - if err := v.BindPFlag("file", flags.Lookup("file")); err != nil { - return err - } - - if err := v.BindPFlag("exclude", flags.Lookup("exclude")); err != nil { - return err - } - - if err := v.BindPFlag("catalogers", flags.Lookup("catalogers")); err != nil { - return err - } - - if err := v.BindPFlag("name", flags.Lookup("name")); err != nil { - return err - } - - if err := v.BindPFlag("source.name", flags.Lookup("source-name")); err != nil { - return err - } - - if err := v.BindPFlag("source.version", flags.Lookup("source-version")); err != nil { - return err - } - - if err := v.BindPFlag("output", flags.Lookup("output")); err != nil { - return err - } - - if err := v.BindPFlag("output-template-path", flags.Lookup("template")); err != nil { - return err - } - - if err := v.BindPFlag("platform", flags.Lookup("platform")); err != nil { - return err - } - - if err := v.BindPFlag("base-path", flags.Lookup("base-path")); err != nil { - return err - } - - return nil -} diff --git a/cmd/syft/cli/options/pkg.go b/cmd/syft/cli/options/pkg.go new file mode 100644 index 000000000..329dad9ed --- /dev/null +++ b/cmd/syft/cli/options/pkg.go @@ -0,0 +1,23 @@ +package options + +import ( + "github.com/anchore/syft/syft/pkg/cataloger" +) + +type pkg struct { + Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` + SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"` + SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"` +} + +func defaultPkg() pkg { + c := cataloger.DefaultSearchConfig() + return pkg{ + SearchIndexedArchives: c.IncludeIndexedArchives, + SearchUnindexedArchives: c.IncludeUnindexedArchives, + Cataloger: scope{ + Enabled: true, + Scope: c.Scope.String(), + }, + } +} diff --git a/internal/config/python.go b/cmd/syft/cli/options/python.go similarity index 50% rename from internal/config/python.go rename to cmd/syft/cli/options/python.go index d86da39be..0efab8713 100644 --- a/internal/config/python.go +++ b/cmd/syft/cli/options/python.go @@ -1,13 +1,5 @@ -package config - -import ( - "github.com/spf13/viper" -) +package options type python struct { GuessUnpinnedRequirements bool `json:"guess-unpinned-requirements" yaml:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"` } - -func (cfg python) loadDefaultValues(v *viper.Viper) { - v.SetDefault("python.guess-unpinned-requirements", false) -} diff --git a/cmd/syft/cli/options/registry.go b/cmd/syft/cli/options/registry.go new file mode 100644 index 000000000..455ba93d5 --- /dev/null +++ b/cmd/syft/cli/options/registry.go @@ -0,0 +1,84 @@ +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 any credential information, use secret type to automatically redact the values + 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("SYFT_REGISTRY_AUTH_AUTHORITY"), + os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"), + os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"), + os.Getenv("SYFT_REGISTRY_AUTH_TOKEN"), + os.Getenv("SYFT_REGISTRY_AUTH_TLS_CERT"), + os.Getenv("SYFT_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. + // since this PostLoad is called before the PostLoad on the Auth credentials list, + // all appropriate redactions will be added + 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/syft/cli/options/registry_test.go similarity index 62% rename from internal/config/registry_test.go rename to cmd/syft/cli/options/registry_test.go index c98511c1e..4979fcbc2 100644 --- a/internal/config/registry_test.go +++ b/cmd/syft/cli/options/registry_test.go @@ -1,4 +1,4 @@ -package config +package options import ( "fmt" @@ -11,48 +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)) }) } } @@ -102,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/syft/cli/options/rekor.go b/cmd/syft/cli/options/rekor.go deleted file mode 100644 index 49539539c..000000000 --- a/cmd/syft/cli/options/rekor.go +++ /dev/null @@ -1,33 +0,0 @@ -package options - -import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -const DefaultRekorURL = "https://rekor.sigstore.dev" - -// RekorOptions is the wrapper for Rekor related options. -type RekorOptions struct { - URL string -} - -var _ Interface = (*RekorOptions)(nil) - -// AddFlags implements Interface -func (o *RekorOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error { - cmd.Flags().StringVar(&o.URL, "rekor-url", DefaultRekorURL, - "address of rekor STL server") - return bindRekorConfigOptions(cmd.Flags(), v) -} - -//nolint:revive -func bindRekorConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error { - // TODO: config re-design - if err := v.BindPFlag("attest.rekor-url", flags.Lookup("rekor-url")); err != nil { - return err - } - - return nil -} diff --git a/cmd/syft/cli/options/root.go b/cmd/syft/cli/options/root.go deleted file mode 100644 index 316fbff17..000000000 --- a/cmd/syft/cli/options/root.go +++ /dev/null @@ -1,37 +0,0 @@ -package options - -import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -type RootOptions struct { - Config string - Quiet bool - Verbose int -} - -var _ Interface = (*RootOptions)(nil) - -func (o *RootOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error { - cmd.PersistentFlags().StringVarP(&o.Config, "config", "c", "", "application config file") - cmd.PersistentFlags().CountVarP(&o.Verbose, "verbose", "v", "increase verbosity (-v = info, -vv = debug)") - cmd.PersistentFlags().BoolVarP(&o.Quiet, "quiet", "q", false, "suppress all logging output") - - return bindRootConfigOptions(cmd.PersistentFlags(), v) -} - -//nolint:revive -func bindRootConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error { - if err := v.BindPFlag("config", flags.Lookup("config")); err != nil { - return err - } - if err := v.BindPFlag("verbosity", flags.Lookup("verbose")); err != nil { - return err - } - if err := v.BindPFlag("quiet", flags.Lookup("quiet")); err != nil { - return err - } - return nil -} diff --git a/cmd/syft/cli/options/scope.go b/cmd/syft/cli/options/scope.go new file mode 100644 index 000000000..ae6efcffe --- /dev/null +++ b/cmd/syft/cli/options/scope.go @@ -0,0 +1,27 @@ +package options + +import ( + "fmt" + + "github.com/anchore/clio" + "github.com/anchore/syft/syft/source" +) + +type scope struct { + Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` + Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` +} + +var _ clio.PostLoader = (*scope)(nil) + +func (opt *scope) PostLoad() error { + s := opt.GetScope() + if s == source.UnknownScope { + return fmt.Errorf("bad scope value %v", opt.Scope) + } + return nil +} + +func (opt scope) GetScope() source.Scope { + return source.ParseScope(opt.Scope) +} diff --git a/cmd/syft/cli/options/secret.go b/cmd/syft/cli/options/secret.go new file mode 100644 index 000000000..3224dd803 --- /dev/null +++ b/cmd/syft/cli/options/secret.go @@ -0,0 +1,25 @@ +package options + +import ( + "fmt" + + "github.com/anchore/clio" + "github.com/anchore/syft/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/internal/config/secrets.go b/cmd/syft/cli/options/secrets.go similarity index 52% rename from internal/config/secrets.go rename to cmd/syft/cli/options/secrets.go index 0d0efc4f1..58693f6e6 100644 --- a/internal/config/secrets.go +++ b/cmd/syft/cli/options/secrets.go @@ -1,29 +1,23 @@ -package config +package options import ( - "github.com/spf13/viper" - "github.com/anchore/syft/internal/file" "github.com/anchore/syft/syft/source" ) type secrets struct { - Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` + Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` AdditionalPatterns map[string]string `yaml:"additional-patterns" json:"additional-patterns" mapstructure:"additional-patterns"` ExcludePatternNames []string `yaml:"exclude-pattern-names" json:"exclude-pattern-names" mapstructure:"exclude-pattern-names"` RevealValues bool `yaml:"reveal-values" json:"reveal-values" mapstructure:"reveal-values"` SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"` } -func (cfg secrets) loadDefaultValues(v *viper.Viper) { - v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault) - v.SetDefault("secrets.cataloger.scope", source.AllLayersScope) - v.SetDefault("secrets.reveal-values", false) - v.SetDefault("secrets.skip-files-above-size", 1*file.MB) - v.SetDefault("secrets.additional-patterns", map[string]string{}) - v.SetDefault("secrets.exclude-pattern-names", []string{}) -} - -func (cfg *secrets) parseConfigValues() error { - return cfg.Cataloger.parseConfigValues() +func defaultSecrets() secrets { + return secrets{ + Cataloger: scope{ + Scope: source.AllLayersScope.String(), + }, + SkipFilesAboveSize: 1 * file.MB, + } } diff --git a/internal/config/source.go b/cmd/syft/cli/options/source.go similarity index 67% rename from internal/config/source.go rename to cmd/syft/cli/options/source.go index 5346f994f..41e301996 100644 --- a/internal/config/source.go +++ b/cmd/syft/cli/options/source.go @@ -1,6 +1,4 @@ -package config - -import "github.com/spf13/viper" +package options type sourceCfg struct { Name string `json:"name" yaml:"name" mapstructure:"name"` @@ -12,6 +10,10 @@ type fileSource struct { Digests []string `json:"digests" yaml:"digests" mapstructure:"digests"` } -func (cfg sourceCfg) loadDefaultValues(v *viper.Viper) { - v.SetDefault("source.file.digests", []string{"sha256"}) +func defaultSourceCfg() sourceCfg { + return sourceCfg{ + File: fileSource{ + Digests: []string{"sha256"}, + }, + } } diff --git a/cmd/syft/cli/options/update_check.go b/cmd/syft/cli/options/update_check.go new file mode 100644 index 000000000..a05b52420 --- /dev/null +++ b/cmd/syft/cli/options/update_check.go @@ -0,0 +1,11 @@ +package options + +type UpdateCheck struct { + 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 +} + +func DefaultUpdateCheck() UpdateCheck { + return UpdateCheck{ + CheckForAppUpdate: true, + } +} diff --git a/cmd/syft/cli/options/verbose.go b/cmd/syft/cli/options/verbose.go deleted file mode 100644 index aa53bb40e..000000000 --- a/cmd/syft/cli/options/verbose.go +++ /dev/null @@ -1,18 +0,0 @@ -package options - -import ( - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/internal/log" -) - -func IsVerbose(app *config.Application) (result bool) { - isPipedInput, err := internal.IsPipedInput() - 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 app.Verbosity > 0 || isPipedInput -} diff --git a/cmd/syft/cli/options/version.go b/cmd/syft/cli/options/version.go deleted file mode 100644 index a3ac49cf6..000000000 --- a/cmd/syft/cli/options/version.go +++ /dev/null @@ -1,17 +0,0 @@ -package options - -import ( - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -type VersionOptions struct { - Output string -} - -var _ Interface = (*VersionOptions)(nil) - -func (o *VersionOptions) AddFlags(cmd *cobra.Command, _ *viper.Viper) error { - cmd.Flags().StringVarP(&o.Output, "output", "o", "text", "format to show version information (available=[text, json])") - return nil -} diff --git a/cmd/syft/cli/options/writer.go b/cmd/syft/cli/options/writer.go index 1ea4ff1a2..047245169 100644 --- a/cmd/syft/cli/options/writer.go +++ b/cmd/syft/cli/options/writer.go @@ -26,9 +26,9 @@ var _ interface { sbom.Writer } = (*sbomStreamWriter)(nil) -// MakeSBOMWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer +// makeSBOMWriter creates a sbom.Writer 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, sbom.Writer.Close() should be called -func MakeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) { +func makeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) { outputOptions, err := parseSBOMOutputFlags(outputs, defaultFile, templateFilePath) if err != nil { return nil, err @@ -42,8 +42,8 @@ func MakeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbo return writer, nil } -// MakeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error. -func MakeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) { +// makeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error. +func makeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) { writer, err := newSBOMMultiWriter(newSBOMWriterDescription(format, path)) if err != nil { return nil, err diff --git a/cmd/syft/cli/options/writer_test.go b/cmd/syft/cli/options/writer_test.go index 643d251cd..dfdc60a39 100644 --- a/cmd/syft/cli/options/writer_test.go +++ b/cmd/syft/cli/options/writer_test.go @@ -34,7 +34,7 @@ func Test_MakeSBOMWriter(t *testing.T) { } for _, tt := range tests { - _, err := MakeSBOMWriter(tt.outputs, "", "") + _, err := makeSBOMWriter(tt.outputs, "", "") tt.wantErr(t, err) } } @@ -183,11 +183,7 @@ func Test_newSBOMMultiWriter(t *testing.T) { switch w := mw.writers[i].(type) { case *sbomStreamWriter: assert.Equal(t, string(w.format.ID()), e.format) - if e.file != "" { - assert.NotNil(t, w.out) - } else { - assert.NotNil(t, w.out) - } + assert.NotNil(t, w.out) if e.file != "" { assert.FileExists(t, tmp+e.file) } diff --git a/cmd/syft/cli/packages.go b/cmd/syft/cli/packages.go deleted file mode 100644 index 88154b199..000000000 --- a/cmd/syft/cli/packages.go +++ /dev/null @@ -1,86 +0,0 @@ -package cli - -import ( - "fmt" - "log" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/cmd/syft/cli/packages" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/config" -) - -const ( - packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages - {{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details - {{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM - {{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM - {{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM - {{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM - {{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM - {{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM - {{.appName}} {{.command}} alpine:latest -vv show verbose debug information - {{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file - - Supports the following image sources: - {{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry. - {{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory -` - - schemeHelpHeader = "You can also explicitly specify the scheme to use:" - imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon - {{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon - {{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) - {{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save" - {{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise) - {{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise) - {{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk -` - nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory) - {{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file) -` - packagesSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp - - packagesHelp = packagesExample + packagesSchemeHelp -) - -//nolint:dupl -func Packages(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "packages [SOURCE]", - Short: "Generate a package SBOM", - Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems", - Example: internal.Tprintf(packagesHelp, map[string]interface{}{ - "appName": internal.ApplicationName, - "command": "packages", - }), - Args: func(cmd *cobra.Command, args []string) error { - if err := app.LoadAllValues(v, ro.Config); err != nil { - return fmt.Errorf("invalid application config: %w", err) - } - // configure logging for command - newLogWrapper(app) - logApplicationConfig(app) - return validateArgs(cmd, args) - }, - SilenceUsage: true, - SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { - if app.CheckForAppUpdate { - // TODO: this is broke, the bus isn't available yet - checkForApplicationUpdate() - } - return packages.Run(cmd.Context(), app, args) - }, - } - - err := po.AddFlags(cmd, v) - if err != nil { - log.Fatal(err) - } - - return cmd -} diff --git a/cmd/syft/cli/packages/packages.go b/cmd/syft/cli/packages/packages.go deleted file mode 100644 index 0f87720fb..000000000 --- a/cmd/syft/cli/packages/packages.go +++ /dev/null @@ -1,192 +0,0 @@ -package packages - -import ( - "context" - "fmt" - - "github.com/wagoodman/go-partybus" - - "github.com/anchore/stereoscope" - "github.com/anchore/stereoscope/pkg/image" - "github.com/anchore/syft/cmd/syft/cli/eventloop" - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/cmd/syft/internal/ui" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/bus" - "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/internal/file" - "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/internal/version" - "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/formats/template" - "github.com/anchore/syft/syft/sbom" - "github.com/anchore/syft/syft/source" -) - -func Run(_ context.Context, app *config.Application, args []string) error { - err := ValidateOutputOptions(app) - if err != nil { - return err - } - - writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath) - if err != nil { - return err - } - - // could be an image or a directory, with or without a scheme - userInput := args[0] - - eventBus := partybus.NewBus() - stereoscope.SetBus(eventBus) - syft.SetBus(eventBus) - subscription := eventBus.Subscribe() - - return eventloop.EventLoop( - execWorker(app, userInput, writer), - eventloop.SetupSignals(), - subscription, - stereoscope.Cleanup, - ui.Select(options.IsVerbose(app), app.Quiet)..., - ) -} - -// nolint:funlen -func execWorker(app *config.Application, userInput string, writer sbom.Writer) <-chan error { - errs := make(chan error) - go func() { - defer close(errs) - defer bus.Exit() - - detection, err := source.Detect( - userInput, - source.DetectConfig{ - DefaultImageSource: app.DefaultImagePullSource, - }, - ) - if err != nil { - errs <- fmt.Errorf("could not deteremine source: %w", err) - return - } - - var platform *image.Platform - - if app.Platform != "" { - platform, err = image.NewPlatform(app.Platform) - if err != nil { - errs <- fmt.Errorf("invalid platform: %w", err) - return - } - } - - hashers, err := file.Hashers(app.Source.File.Digests...) - if err != nil { - errs <- fmt.Errorf("invalid hash: %w", err) - return - } - - src, err := detection.NewSource( - source.DetectionSourceConfig{ - Alias: source.Alias{ - Name: app.Source.Name, - Version: app.Source.Version, - }, - RegistryOptions: app.Registry.ToOptions(), - Platform: platform, - Exclude: source.ExcludeConfig{ - Paths: app.Exclusions, - }, - DigestAlgorithms: hashers, - BasePath: app.BasePath, - }, - ) - - if err != nil { - errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) - return - } - - defer func() { - if src != nil { - if err := src.Close(); err != nil { - log.Tracef("unable to close source: %+v", err) - } - } - }() - - s, err := GenerateSBOM(src, errs, app) - if err != nil { - errs <- err - return - } - - if s == nil { - errs <- fmt.Errorf("no SBOM produced for %q", userInput) - return - } - - if err := writer.Write(*s); err != nil { - errs <- fmt.Errorf("failed to write SBOM: %w", err) - return - } - }() - return errs -} - -func GenerateSBOM(src source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) { - tasks, err := eventloop.Tasks(app) - if err != nil { - return nil, err - } - - s := sbom.SBOM{ - Source: src.Describe(), - Descriptor: sbom.Descriptor{ - Name: internal.ApplicationName, - Version: version.FromBuild().Version, - Configuration: app, - }, - } - - buildRelationships(&s, src, tasks, errs) - - return &s, nil -} - -func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task, errs chan error) { - var relationships []<-chan artifact.Relationship - for _, task := range tasks { - c := make(chan artifact.Relationship) - relationships = append(relationships, c) - go eventloop.RunTask(task, &s.Artifacts, src, c, errs) - } - - s.Relationships = append(s.Relationships, MergeRelationships(relationships...)...) -} - -func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) { - for _, c := range cs { - for n := range c { - relationships = append(relationships, n) - } - } - - return relationships -} - -func ValidateOutputOptions(app *config.Application) error { - var usesTemplateOutput bool - for _, o := range app.Outputs { - if o == template.ID.String() { - usesTemplateOutput = true - break - } - } - - if usesTemplateOutput && app.OutputTemplatePath == "" { - return fmt.Errorf(`must specify path to template file when using "template" output format`) - } - - return nil -} diff --git a/cmd/syft/cli/poweruser.go b/cmd/syft/cli/poweruser.go deleted file mode 100644 index e3c935d9e..000000000 --- a/cmd/syft/cli/poweruser.go +++ /dev/null @@ -1,51 +0,0 @@ -package cli - -import ( - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/cmd/syft/cli/poweruser" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/config" -) - -const powerUserExample = ` {{.appName}} {{.command}} - DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0 - Only image sources are supported (e.g. docker: , podman: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported, template outputs are not supported. - All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration) -` - -func PowerUser(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "power-user [IMAGE]", - Short: "Run bulk operations on container images", - Example: internal.Tprintf(powerUserExample, map[string]interface{}{ - "appName": internal.ApplicationName, - "command": "power-user", - }), - Args: func(cmd *cobra.Command, args []string) error { - if err := app.LoadAllValues(v, ro.Config); err != nil { - return fmt.Errorf("invalid application config: %w", err) - } - // configure logging for command - newLogWrapper(app) - logApplicationConfig(app) - return validateArgs(cmd, args) - }, - Hidden: true, - SilenceUsage: true, - SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { - if app.CheckForAppUpdate { - checkForApplicationUpdate() - // TODO: this is broke, the bus isn't available yet - } - return poweruser.Run(cmd.Context(), app, args) - }, - } - - return cmd -} diff --git a/cmd/syft/cli/poweruser/poweruser.go b/cmd/syft/cli/poweruser/poweruser.go deleted file mode 100644 index cfc10e1bc..000000000 --- a/cmd/syft/cli/poweruser/poweruser.go +++ /dev/null @@ -1,144 +0,0 @@ -package poweruser - -import ( - "context" - "fmt" - "os" - - "github.com/gookit/color" - "github.com/wagoodman/go-partybus" - - "github.com/anchore/stereoscope" - "github.com/anchore/stereoscope/pkg/image" - "github.com/anchore/syft/cmd/syft/cli/eventloop" - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/cmd/syft/cli/packages" - "github.com/anchore/syft/cmd/syft/internal/ui" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/bus" - "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/internal/version" - "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/formats/syftjson" - "github.com/anchore/syft/syft/sbom" - "github.com/anchore/syft/syft/source" -) - -func Run(_ context.Context, app *config.Application, args []string) error { - f := syftjson.Format() - writer, err := options.MakeSBOMWriterForFormat(f, app.File) - if err != nil { - return err - } - defer func() { - // inform user at end of run that command will be removed - deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0") - fmt.Fprintln(os.Stderr, deprecated) - }() - - userInput := args[0] - - eventBus := partybus.NewBus() - stereoscope.SetBus(eventBus) - syft.SetBus(eventBus) - subscription := eventBus.Subscribe() - - return eventloop.EventLoop( - execWorker(app, userInput, writer), - eventloop.SetupSignals(), - subscription, - stereoscope.Cleanup, - ui.Select(options.IsVerbose(app), app.Quiet)..., - ) -} - -//nolint:funlen -func execWorker(app *config.Application, userInput string, writer sbom.Writer) <-chan error { - errs := make(chan error) - go func() { - defer close(errs) - defer bus.Exit() - - app.Secrets.Cataloger.Enabled = true - app.FileMetadata.Cataloger.Enabled = true - app.FileContents.Cataloger.Enabled = true - app.FileClassification.Cataloger.Enabled = true - tasks, err := eventloop.Tasks(app) - if err != nil { - errs <- err - return - } - - detection, err := source.Detect( - userInput, - source.DetectConfig{ - DefaultImageSource: app.DefaultImagePullSource, - }, - ) - if err != nil { - errs <- fmt.Errorf("could not deteremine source: %w", err) - return - } - - var platform *image.Platform - - if app.Platform != "" { - platform, err = image.NewPlatform(app.Platform) - if err != nil { - errs <- fmt.Errorf("invalid platform: %w", err) - return - } - } - - src, err := detection.NewSource( - source.DetectionSourceConfig{ - Alias: source.Alias{ - Name: app.Source.Name, - Version: app.Source.Version, - }, - RegistryOptions: app.Registry.ToOptions(), - Platform: platform, - Exclude: source.ExcludeConfig{ - Paths: app.Exclusions, - }, - DigestAlgorithms: nil, - BasePath: app.BasePath, - }, - ) - - if src != nil { - defer src.Close() - } - if err != nil { - errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) - return - } - - s := sbom.SBOM{ - Source: src.Describe(), - Descriptor: sbom.Descriptor{ - Name: internal.ApplicationName, - Version: version.FromBuild().Version, - Configuration: app, - }, - } - - var relationships []<-chan artifact.Relationship - for _, task := range tasks { - c := make(chan artifact.Relationship) - relationships = append(relationships, c) - - go eventloop.RunTask(task, &s.Artifacts, src, c, errs) - } - - s.Relationships = append(s.Relationships, packages.MergeRelationships(relationships...)...) - - if err := writer.Write(s); err != nil { - errs <- fmt.Errorf("failed to write sbom: %w", err) - return - } - }() - - return errs -} diff --git a/cmd/syft/cli/ui/handle_pull_containerd_image.go b/cmd/syft/cli/ui/handle_pull_containerd_image.go new file mode 100644 index 000000000..0b3929deb --- /dev/null +++ b/cmd/syft/cli/ui/handle_pull_containerd_image.go @@ -0,0 +1,190 @@ +package ui + +import ( + "fmt" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/dustin/go-humanize" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + stereoscopeParsers "github.com/anchore/stereoscope/pkg/event/parsers" + "github.com/anchore/stereoscope/pkg/image/containerd" + "github.com/anchore/syft/internal/log" +) + +var _ interface { + progress.Stager + progress.Progressable +} = (*containerdPullProgressAdapter)(nil) + +type containerdPullStatus interface { + Complete() bool + Layers() []containerd.LayerID + Current(containerd.LayerID) progress.Progressable +} + +type containerdPullProgressAdapter struct { + status containerdPullStatus + formatter containerdPullStatusFormatter +} + +type containerdPullStatusFormatter struct { + auxInfoStyle lipgloss.Style + pullCompletedStyle lipgloss.Style + pullDownloadStyle lipgloss.Style + pullStageChars []string + layerCaps []string +} + +func (m *Handler) handlePullContainerdImage(e partybus.Event) []tea.Model { + _, pullStatus, err := stereoscopeParsers.ParsePullContainerdImage(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil + } + + if pullStatus == nil { + return nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "Pull image", + Running: "Pulling image", + Success: "Pulled image", + }, + taskprogress.WithStagedProgressable( + newContainerdPullProgressAdapter(pullStatus), + ), + ) + + tsk.HintStyle = lipgloss.NewStyle() + tsk.HintEndCaps = nil + + return []tea.Model{tsk} +} + +func newContainerdPullProgressAdapter(status *containerd.PullStatus) *containerdPullProgressAdapter { + return &containerdPullProgressAdapter{ + status: status, + formatter: newContainerdPullStatusFormatter(), + } +} + +func newContainerdPullStatusFormatter() containerdPullStatusFormatter { + return containerdPullStatusFormatter{ + auxInfoStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")), + pullCompletedStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#fcba03")), + pullDownloadStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")), + pullStageChars: strings.Split("▁▃▄▅▆▇█", ""), + layerCaps: strings.Split("▕▏", ""), + } +} + +func (d containerdPullProgressAdapter) Size() int64 { + return -1 +} + +func (d containerdPullProgressAdapter) Current() int64 { + return 1 +} + +func (d containerdPullProgressAdapter) Error() error { + if d.status.Complete() { + return progress.ErrCompleted + } + // TODO: return intermediate error indications + return nil +} + +func (d containerdPullProgressAdapter) Stage() string { + return d.formatter.Render(d.status) +} + +// Render crafts the given docker image pull status summarized into a single line. +func (f containerdPullStatusFormatter) Render(pullStatus containerdPullStatus) string { + var size, current uint64 + + layers := pullStatus.Layers() + status := make(map[containerd.LayerID]progress.Progressable) + completed := make([]string, len(layers)) + + // fetch the current state + for idx, layer := range layers { + completed[idx] = " " + status[layer] = pullStatus.Current(layer) + } + + numCompleted := 0 + for idx, layer := range layers { + prog := status[layer] + curN := prog.Current() + curSize := prog.Size() + + if progress.IsCompleted(prog) { + input := f.pullStageChars[len(f.pullStageChars)-1] + completed[idx] = f.formatPullPhase(prog.Error() != nil, input) + } else if curN != 0 { + var ratio float64 + switch { + case curN == 0 || curSize < 0: + ratio = 0 + case curN >= curSize: + ratio = 1 + default: + ratio = float64(curN) / float64(curSize) + } + + i := int(ratio * float64(len(f.pullStageChars)-1)) + input := f.pullStageChars[i] + completed[idx] = f.formatPullPhase(status[layer].Error() != nil, input) + } + + if progress.IsErrCompleted(status[layer].Error()) { + numCompleted++ + } + } + + for _, layer := range layers { + prog := status[layer] + size += uint64(prog.Size()) + current += uint64(prog.Current()) + } + + var progStr, auxInfo string + if len(layers) > 0 { + render := strings.Join(completed, "") + prefix := f.pullCompletedStyle.Render(fmt.Sprintf("%d Layers", len(layers))) + auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s / %s]", humanize.Bytes(current), humanize.Bytes(size))) + if len(layers) == numCompleted { + auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s] Extracting...", humanize.Bytes(size))) + } + + progStr = fmt.Sprintf("%s%s%s%s", prefix, f.layerCap(false), render, f.layerCap(true)) + } + + return progStr + auxInfo +} + +// formatPullPhase returns a single character that represents the status of a layer pull. +func (f containerdPullStatusFormatter) formatPullPhase(completed bool, inputStr string) string { + if completed { + return f.pullCompletedStyle.Render(f.pullStageChars[len(f.pullStageChars)-1]) + } + return f.pullDownloadStyle.Render(inputStr) +} + +func (f containerdPullStatusFormatter) layerCap(end bool) string { + l := len(f.layerCaps) + if l == 0 { + return "" + } + if end { + return f.layerCaps[l-1] + } + return f.layerCaps[0] +} diff --git a/cmd/syft/cli/ui/handler.go b/cmd/syft/cli/ui/handler.go index 96d078c0c..b2054467f 100644 --- a/cmd/syft/cli/ui/handler.go +++ b/cmd/syft/cli/ui/handler.go @@ -49,6 +49,7 @@ func New(cfg HandlerConfig) *Handler { // register all supported event types with the respective handler functions d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{ stereoscopeEvent.PullDockerImage: h.handlePullDockerImage, + stereoscopeEvent.PullContainerdImage: h.handlePullContainerdImage, stereoscopeEvent.ReadImage: h.handleReadImage, stereoscopeEvent.FetchImage: h.handleFetchImage, syftEvent.PackageCatalogerStarted: h.handlePackageCatalogerStarted, diff --git a/cmd/syft/cli/version.go b/cmd/syft/cli/version.go deleted file mode 100644 index 3235a813b..000000000 --- a/cmd/syft/cli/version.go +++ /dev/null @@ -1,72 +0,0 @@ -package cli - -import ( - "encoding/json" - "fmt" - "log" - "os" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/internal/version" -) - -func Version(v *viper.Viper, _ *config.Application) *cobra.Command { - o := &options.VersionOptions{} - cmd := &cobra.Command{ - Use: "version", - Short: "show the version", - RunE: func(cmd *cobra.Command, args []string) error { - return printVersion(o.Output) - }, - } - - err := o.AddFlags(cmd, v) - if err != nil { - log.Fatal(err) - } - - return cmd -} - -func printVersion(output string) error { - versionInfo := version.FromBuild() - - switch output { - case "text": - fmt.Println("Application: ", internal.ApplicationName) - fmt.Println("Version: ", versionInfo.Version) - fmt.Println("JsonSchemaVersion: ", internal.JSONSchemaVersion) - 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) - - case "json": - enc := json.NewEncoder(os.Stdout) - enc.SetEscapeHTML(false) - enc.SetIndent("", " ") - err := enc.Encode(&struct { - version.Version - Application string `json:"application"` - }{ - Version: versionInfo, - Application: internal.ApplicationName, - }) - if err != nil { - fmt.Printf("failed to show version information: %+v\n", err) - os.Exit(1) - } - default: - fmt.Printf("unsupported output format: %s\n", output) - os.Exit(1) - } - - return nil -} diff --git a/cmd/syft/internal/constants.go b/cmd/syft/internal/constants.go new file mode 100644 index 000000000..eedbdb0ee --- /dev/null +++ b/cmd/syft/internal/constants.go @@ -0,0 +1,5 @@ +package internal + +const ( + NotProvided = "[not provided]" +) diff --git a/cmd/syft/internal/ui/__snapshots__/post_ui_event_writer_test.snap b/cmd/syft/internal/ui/__snapshots__/post_ui_event_writer_test.snap index bf473e331..64cafcab3 100755 --- a/cmd/syft/internal/ui/__snapshots__/post_ui_event_writer_test.snap +++ b/cmd/syft/internal/ui/__snapshots__/post_ui_event_writer_test.snap @@ -27,12 +27,7 @@ report 1!!> - - - - - +A newer version of syft is available for download: v0.33.0 (installed version is [not provided]) --- diff --git a/cmd/syft/internal/ui/no_ui.go b/cmd/syft/internal/ui/no_ui.go index 015ae8218..e8ad13245 100644 --- a/cmd/syft/internal/ui/no_ui.go +++ b/cmd/syft/internal/ui/no_ui.go @@ -33,8 +33,6 @@ func (n *NoUI) Handle(e partybus.Event) error { case event.CLIReport, event.CLINotification: // keep these for when the UI is terminated to show to the screen (or perform other events) n.finalizeEvents = append(n.finalizeEvents, e) - case event.CLIExit: - return n.subscription.Unsubscribe() } return nil } diff --git a/cmd/syft/internal/ui/post_ui_event_writer.go b/cmd/syft/internal/ui/post_ui_event_writer.go index e3772981b..dcef5cb6c 100644 --- a/cmd/syft/internal/ui/post_ui_event_writer.go +++ b/cmd/syft/internal/ui/post_ui_event_writer.go @@ -118,12 +118,19 @@ func writeAppUpdate(writer io.Writer, events ...partybus.Event) error { style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true) for _, e := range events { - notice, err := parsers.ParseCLIAppUpdateAvailable(e) + updateCheck, err := parsers.ParseCLIAppUpdateAvailable(e) if err != nil { log.WithFields("error", err).Warn("failed to parse app update notification") continue } + if updateCheck.Current == updateCheck.New { + log.Tracef("update check event with identical versions: %s", updateCheck.Current) + continue + } + + notice := fmt.Sprintf("A newer version of syft is available for download: %s (installed version is %s)", updateCheck.New, updateCheck.Current) + if _, err := fmt.Fprintln(writer, style.Render(notice)); err != nil { // don't let this be fatal log.WithFields("error", err).Warn("failed to write app update notification") diff --git a/cmd/syft/internal/ui/post_ui_event_writer_test.go b/cmd/syft/internal/ui/post_ui_event_writer_test.go index a5bdd5792..45372e570 100644 --- a/cmd/syft/internal/ui/post_ui_event_writer_test.go +++ b/cmd/syft/internal/ui/post_ui_event_writer_test.go @@ -9,6 +9,7 @@ import ( "github.com/wagoodman/go-partybus" "github.com/anchore/syft/syft/event" + "github.com/anchore/syft/syft/event/parsers" ) func Test_postUIEventWriter_write(t *testing.T) { @@ -34,8 +35,11 @@ func Test_postUIEventWriter_write(t *testing.T) { Value: "", }, { - Type: event.CLIAppUpdateAvailable, - Value: "\n\n\n\n", + Type: event.CLIAppUpdateAvailable, + Value: parsers.UpdateCheck{ + New: "v0.33.0", + Current: "[not provided]", + }, }, { Type: event.CLINotification, @@ -61,8 +65,11 @@ func Test_postUIEventWriter_write(t *testing.T) { Value: "", }, { - Type: event.CLIAppUpdateAvailable, - Value: "", + Type: event.CLIAppUpdateAvailable, + Value: parsers.UpdateCheck{ + New: "", + Current: "", + }, }, { Type: event.CLIReport, diff --git a/cmd/syft/internal/ui/select.go b/cmd/syft/internal/ui/select.go deleted file mode 100644 index 27b536192..000000000 --- a/cmd/syft/internal/ui/select.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build linux || darwin || netbsd -// +build linux darwin netbsd - -package ui - -import ( - "os" - "runtime" - - "golang.org/x/term" - - "github.com/anchore/clio" - handler "github.com/anchore/syft/cmd/syft/cli/ui" -) - -// Select is responsible for determining the specific UI function given select user option, the current platform -// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs -// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there -// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of -// the final SBOM report. -func Select(verbose, quiet bool) (uis []clio.UI) { - isStdoutATty := term.IsTerminal(int(os.Stdout.Fd())) - isStderrATty := term.IsTerminal(int(os.Stderr.Fd())) - notATerminal := !isStderrATty && !isStdoutATty - - switch { - case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty: - uis = append(uis, None(quiet)) - default: - // TODO: it may make sense in the future to pass handler options into select - h := handler.New(handler.DefaultHandlerConfig()) - uis = append(uis, New(h, verbose, quiet)) - } - - return uis -} diff --git a/cmd/syft/internal/ui/select_windows.go b/cmd/syft/internal/ui/select_windows.go deleted file mode 100644 index 0408be53b..000000000 --- a/cmd/syft/internal/ui/select_windows.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build windows -// +build windows - -package ui - -import "github.com/anchore/clio" - -// Select is responsible for determining the specific UI function given select user option, the current platform -// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs -// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there -// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of -// the final SBOM report. -func Select(verbose, quiet bool) (uis []clio.UI) { - return append(uis, None(quiet)) -} diff --git a/cmd/syft/internal/ui/ui.go b/cmd/syft/internal/ui/ui.go index 441470b7c..cd31cc4e1 100644 --- a/cmd/syft/internal/ui/ui.go +++ b/cmd/syft/internal/ui/ui.go @@ -1,16 +1,18 @@ 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" - handler "github.com/anchore/syft/cmd/syft/cli/ui" "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/event" @@ -29,13 +31,13 @@ type UI struct { subscription partybus.Unsubscribable finalizeEvents []partybus.Event - handler *handler.Handler + handler *bubbly.HandlerCollection frame tea.Model } -func New(h *handler.Handler, _, quiet bool) *UI { +func New(quiet bool, handlers ...bubbly.EventHandler) *UI { return &UI{ - handler: h, + handler: bubbly.NewHandlerCollection(handlers...), frame: frame.New(), running: &sync.WaitGroup{}, quiet: quiet, @@ -49,38 +51,30 @@ func (m *UI) Setup(subscription partybus.Unsubscribable) error { } m.subscription = subscription - m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin)) + m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin), tea.WithoutSignalHandler()) 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) - m.exit() + bus.ExitWithInterrupt() } }() return nil } -func (m *UI) exit() { - // stop the event loop - bus.Exit() -} - func (m *UI) Handle(e partybus.Event) error { if m.program != nil { m.program.Send(e) - if e.Type == event.CLIExit { - return m.subscription.Unsubscribe() - } } return nil } func (m *UI) Teardown(force bool) error { if !force { - m.handler.Running.Wait() + 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 @@ -88,7 +82,19 @@ func (m *UI) Teardown(force bool) error { // string from the worker (outside of the UI after teardown). m.running.Wait() } else { - m.program.Kill() + _ = runWithTimeout(250*time.Millisecond, func() error { + m.handler.Wait() + return nil + }) + + // it may be tempting to use Kill() however it has been found that this can cause the terminal to be left in + // a bad state (where Ctrl+C and other control characters no longer works for future processes in that terminal). + m.program.Quit() + + _ = 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) @@ -107,7 +113,6 @@ func (m UI) RespondsTo() []partybus.EventType { return append([]partybus.EventType{ event.CLIReport, event.CLINotification, - event.CLIExit, event.CLIAppUpdateAvailable, }, m.handler.RespondsTo()...) } @@ -126,8 +131,10 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { + // today we treat esc and ctrl+c the same, but in the future when the worker has a graceful way to + // cancel in-flight work via a context, we can wire up esc to this path with bus.Exit() case "esc", "ctrl+c": - m.exit() + bus.ExitWithInterrupt() return m, tea.Quit } @@ -135,12 +142,12 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { log.WithFields("component", "ui").Tracef("event: %q", msg.Type) switch msg.Type { - case event.CLIReport, event.CLINotification, event.CLIExit, event.CLIAppUpdateAvailable: + case event.CLIReport, event.CLINotification, event.CLIAppUpdateAvailable: // keep these for when the UI is terminated to show to the screen (or perform other events) m.finalizeEvents = append(m.finalizeEvents, msg) // why not return tea.Quit here for exit events? because there may be UI components that still need the update-render loop. - // for this reason we'll let the syft event loop call Teardown() which will explicitly wait for these components + // for this reason we'll let the event loop call Teardown() which will explicitly wait for these components return m, nil } @@ -164,3 +171,17 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m UI) View() string { return m.frame.View() } + +func runWithTimeout(timeout time.Duration, fn func() error) (err error) { + c := make(chan struct{}, 1) + go func() { + err = fn() + c <- struct{}{} + }() + select { + case <-c: + case <-time.After(timeout): + return fmt.Errorf("timed out after %v", timeout) + } + return err +} diff --git a/cmd/syft/main.go b/cmd/syft/main.go index 1a4b9fb7c..d1f303d0e 100644 --- a/cmd/syft/main.go +++ b/cmd/syft/main.go @@ -1,20 +1,34 @@ package main import ( - "log" - _ "modernc.org/sqlite" + "github.com/anchore/clio" "github.com/anchore/syft/cmd/syft/cli" + "github.com/anchore/syft/cmd/syft/internal" +) + +// applicationName is the non-capitalized name of the application (do not change this) +const applicationName = "syft" + +// 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() { - cli, err := cli.New() - if err != nil { - log.Fatalf("error during command construction: %v", err) - } + app := cli.Application( + clio.Identification{ + Name: applicationName, + Version: version, + BuildDate: buildDate, + GitCommit: gitCommit, + GitDescription: gitDescription, + }, + ) - if err := cli.Execute(); err != nil { - log.Fatalf("error during command execution: %v", err) - } + app.Run() } diff --git a/go.mod b/go.mod index c67d868d2..182bdcde0 100644 --- a/go.mod +++ b/go.mod @@ -1,109 +1,121 @@ module github.com/anchore/syft -go 1.19 +go 1.21.0 require ( + github.com/CycloneDX/cyclonedx-go v0.7.2 + github.com/Masterminds/semver v1.5.0 + github.com/Masterminds/sprig/v3 v3.2.3 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/acobaugh/osrelease v0.1.0 - github.com/adrg/xdg v0.4.0 + github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 + github.com/anchore/clio v0.0.0-20230823172630-c42d666061af + github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe + github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 + github.com/anchore/stereoscope v0.0.0-20230925132944-bf05af58eb44 + // we are hinting brotli to latest due to warning when installing archiver v3: + // go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption + github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 github.com/bmatcuk/doublestar/v4 v4.6.0 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.9.1 + github.com/dave/jennifer v1.7.0 + github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da + github.com/docker/distribution v2.8.3+incompatible // indirect + 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/github/go-spdx/v2 v2.2.0 + github.com/gkampitakis/go-snaps v0.4.11 + github.com/go-git/go-billy/v5 v5.5.0 + github.com/go-git/go-git/v5 v5.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/google/go-cmp v0.6.0 + github.com/google/go-containerregistry v0.16.1 + github.com/google/licensecheck v0.3.1 + github.com/google/uuid v1.3.1 github.com/gookit/color v1.5.4 github.com/hashicorp/go-multierror v1.1.1 - github.com/jinzhu/copier v0.3.5 - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/iancoleman/strcase v0.3.0 + github.com/invopop/jsonschema v0.7.0 + github.com/jinzhu/copier v0.4.0 + github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 + github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b github.com/mholt/archiver/v3 v3.5.1 github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 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/opencontainers/go-digest v1.0.0 github.com/pelletier/go-toml v1.9.5 + github.com/saferwall/pe v1.4.7 + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d + github.com/sassoftware/go-rpmutils v0.2.0 // pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5 github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e github.com/sergi/go-diff v1.3.1 - github.com/sirupsen/logrus v1.9.3 github.com/spdx/tools-golang v0.5.3 - github.com/spf13/afero v1.9.5 + 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.16.0 github.com/stretchr/testify v1.8.4 + github.com/vbatts/go-mtree v0.5.3 github.com/vifraa/gopom v1.0.0 github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 - github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 + github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/mod v0.12.0 - golang.org/x/net v0.14.0 - golang.org/x/term v0.11.0 - gopkg.in/yaml.v2 v2.4.0 + github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 + golang.org/x/mod v0.13.0 + golang.org/x/net v0.17.0 + golang.org/x/term v0.13.0 // indirect + gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.26.0 ) require ( - github.com/CycloneDX/cyclonedx-go v0.7.1 - github.com/Masterminds/semver v1.5.0 - github.com/Masterminds/sprig/v3 v3.2.3 - github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 - github.com/anchore/clio v0.0.0-20230602170917-e747e60c4aa0 - github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe - github.com/anchore/stereoscope v0.0.0-20230727211946-d1f3d766295e - github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 - github.com/charmbracelet/bubbletea v0.24.2 - github.com/charmbracelet/lipgloss v0.7.1 - github.com/dave/jennifer v1.7.0 - github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da - github.com/docker/distribution v2.8.2+incompatible - github.com/docker/docker v24.0.5+incompatible - github.com/github/go-spdx/v2 v2.1.2 - github.com/gkampitakis/go-snaps v0.4.7 - github.com/go-git/go-billy/v5 v5.4.1 - github.com/go-git/go-git/v5 v5.8.1 - github.com/google/go-containerregistry v0.16.1 - github.com/google/licensecheck v0.3.1 - github.com/invopop/jsonschema v0.7.0 - github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b - github.com/opencontainers/go-digest v1.0.0 - github.com/saferwall/pe v1.4.4 - github.com/sassoftware/go-rpmutils v0.2.0 - github.com/vbatts/go-mtree v0.5.3 - github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 - golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b - gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.25.0 + github.com/distribution/reference v0.5.0 + github.com/sanity-io/litter v1.5.5 ) require ( 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/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/DataDog/zstd v1.4.5 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect + github.com/Microsoft/hcsshim v0.10.0-rc.7 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect - github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba // indirect + github.com/adrg/xdg v0.4.0 // indirect github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/becheran/wildmatch-go v1.0.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/docker/cli v24.0.0+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 @@ -111,9 +123,11 @@ require ( github.com/felixge/fgprof v0.9.3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.0 // indirect - github.com/gkampitakis/ciinfo v0.2.4 // indirect + github.com/gkampitakis/ciinfo v0.2.5 // indirect github.com/gkampitakis/go-diff v1.3.2 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-logr/logr v1.2.3 // 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 @@ -139,34 +153,45 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/copystructure v1.2.0 // 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/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.1 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/nwaples/rardecode v1.1.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/pborman/indent v1.2.1 // 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/pkg/profile v1.7.0 // 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.9.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.0 // 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/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.16.0 // indirect github.com/subosito/gotenv v1.4.2 // 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.14.4 // 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 @@ -178,10 +203,14 @@ require ( github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect - golang.org/x/sync v0.2.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect - golang.org/x/tools v0.9.1 // 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 + golang.org/x/crypto v0.14.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/tools v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.55.0 // indirect @@ -199,14 +228,6 @@ require ( modernc.org/token v1.0.1 // indirect ) -require ( - // we are hinting brotli to latest due to warning when installing archiver v3: - // go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption - github.com/andybalholm/brotli v1.0.4 // indirect - github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.12.0 // indirect -) - retract ( v0.53.2 v0.53.1 // Published accidentally with incorrect license in depdencies diff --git a/go.sum b/go.sum index 7ae75b12f..90b12ccbf 100644 --- a/go.sum +++ b/go.sum @@ -51,14 +51,18 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f 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= @@ -73,9 +77,11 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa 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-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +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= @@ -90,12 +96,12 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 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-20230602170917-e747e60c4aa0 h1:g0UqRW60JDrf5fb40RUyIwwcfQ3nAJqGj4aUCVTwFE4= -github.com/anchore/clio v0.0.0-20230602170917-e747e60c4aa0/go.mod h1:0IQVIROfgRX4WZFMfgsbNZmMgLKqW/KgByyJDYvWiDE= -github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba h1:tJ186HK8e0Lf+hhNWX4fJrq14yj3mw8JQkkLhA0nFhE= -github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba/go.mod h1:E3zNHEz7mizIFGJhuX+Ga7AbCmEN5TfzVDxmOfj7XZw= -github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe h1:Df867YMmymdMG6z5IW8pR0/2CRpLIjYnaTXLp6j+s0k= -github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= +github.com/anchore/clio v0.0.0-20230823172630-c42d666061af h1:dBVKZyMZeA0oZK0+aCCRoqxhxUvx/7xy/VEaLMMMnb0= +github.com/anchore/clio v0.0.0-20230823172630-c42d666061af/go.mod h1:XryJ3CIF1T7SbacQV+OPykfKKIbfXnBssYfpjy2peUg= +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= @@ -106,13 +112,14 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/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/stereoscope v0.0.0-20230727211946-d1f3d766295e h1:S6IhYpsBCpvphlHA1tN0glSG/kjVvFzC6OJuU2qW5Pc= -github.com/anchore/stereoscope v0.0.0-20230727211946-d1f3d766295e/go.mod h1:0LsgHgXO4QFnk2hsYwtqd3fR18PIZXlFLIl2qb9tu3g= +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/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/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/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= @@ -124,6 +131,7 @@ github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/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= @@ -135,6 +143,7 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB 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/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= @@ -147,11 +156,13 @@ github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06 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.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +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/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= @@ -167,36 +178,56 @@ 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/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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.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.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY= -github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +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= @@ -206,7 +237,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp 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-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +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= @@ -230,30 +262,35 @@ 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.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.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.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/github/go-spdx/v2 v2.1.2 h1:p+Tv0yMgcuO0/vnMe9Qh4tmUgYhI6AsLVlakZ/Sx+DM= -github.com/github/go-spdx/v2 v2.1.2/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w= -github.com/gkampitakis/ciinfo v0.2.4 h1:Ip1hf4K7ISRuVlDrheuhaeffg1VOhlyeFGaQ/vTxrtE= -github.com/gkampitakis/ciinfo v0.2.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/github/go-spdx/v2 v2.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.7 h1:w1hOUdiKELxiTcRySQLmLuV3KW40mIRw3rzcYJGrb3I= -github.com/gkampitakis/go-snaps v0.4.7/go.mod h1:8HW4KX3JKV8M0GSw69CvT+Jqhd1AlBPMPpBfjBI3bdY= +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.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= +github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= 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.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-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/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= -github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo= +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= @@ -261,6 +298,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.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/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= @@ -268,6 +310,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me 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= @@ -323,8 +366,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -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-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= @@ -355,8 +399,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S3 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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/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= @@ -406,6 +450,8 @@ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= 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= @@ -419,8 +465,8 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77 github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= -github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -428,6 +474,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/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y= +github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -486,9 +534,10 @@ github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZ github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 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= @@ -516,31 +565,52 @@ 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/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.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/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.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/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/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= @@ -564,6 +634,7 @@ github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -580,6 +651,9 @@ 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= @@ -588,20 +662,27 @@ 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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 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.4 h1:Ml++7/2/Z1iKwV4zCsd1nIqTEAdUQKAetwbbcCarhOg= -github.com/saferwall/pe v1.4.4/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s= +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= @@ -612,6 +693,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV 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.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -623,8 +705,8 @@ github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQr 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.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= @@ -644,6 +726,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -664,11 +747,14 @@ 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.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/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= @@ -681,6 +767,7 @@ github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oW 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= @@ -689,10 +776,12 @@ github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RV github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= 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/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-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/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/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-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -726,10 +815,17 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 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 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 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/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -750,8 +846,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 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.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +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= @@ -762,8 +858,6 @@ 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/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= @@ -777,7 +871,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -793,8 +886,8 @@ 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -843,8 +936,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +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= @@ -875,8 +968,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.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= @@ -890,6 +983,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-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -897,6 +991,7 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -940,10 +1035,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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -958,16 +1055,16 @@ 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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +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= @@ -980,12 +1077,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.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= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -1044,8 +1142,8 @@ 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +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= @@ -1222,13 +1320,13 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= @@ -1243,7 +1341,9 @@ modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= 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 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= @@ -1252,14 +1352,16 @@ modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA= -modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= +modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw= +modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/bus/helpers.go b/internal/bus/helpers.go index 5efacfb35..b2c883ebd 100644 --- a/internal/bus/helpers.go +++ b/internal/bus/helpers.go @@ -3,21 +3,24 @@ package bus import ( "github.com/wagoodman/go-partybus" - "github.com/anchore/syft/internal/log" + "github.com/anchore/clio" + "github.com/anchore/syft/internal/redact" "github.com/anchore/syft/syft/event" ) func Exit() { - Publish(partybus.Event{ - Type: event.CLIExit, - }) + Publish(clio.ExitEvent(false)) +} + +func ExitWithInterrupt() { + Publish(clio.ExitEvent(true)) } func Report(report string) { if len(report) == 0 { return } - report = log.Redactor.RedactString(report) + report = redact.Apply(report) Publish(partybus.Event{ Type: event.CLIReport, Value: report, diff --git a/internal/config/application.go b/internal/config/application.go deleted file mode 100644 index e7d6539fe..000000000 --- a/internal/config/application.go +++ /dev/null @@ -1,337 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "os" - "path" - "reflect" - "sort" - "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/syft/internal" - "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/syft/pkg/cataloger" - golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang" - "github.com/anchore/syft/syft/pkg/cataloger/kernel" - pythonCataloger "github.com/anchore/syft/syft/pkg/cataloger/python" -) - -var ( - ErrApplicationConfigNotFound = fmt.Errorf("application config not found") - catalogerEnabledDefault = false -) - -type defaultValueLoader interface { - loadDefaultValues(*viper.Viper) -} - -type parser interface { - parseConfigValues() error -} - -// Application is the main syft application configuration. -type Application struct { - // the location where the application config was read from (either from -c or discovered while loading); default .syft.yaml - ConfigPath string `yaml:"configPath,omitempty" json:"configPath" mapstructure:"config"` - Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"` - // -q, indicates to not show any status output to stderr (ETUI or logging UI) - Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` - Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output - OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output - File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to - 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 - Dev development `yaml:"dev" json:"dev" mapstructure:"dev"` - Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options - Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"` - Package pkg `yaml:"package" json:"package" mapstructure:"package"` - Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"` - LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"` - Python python `yaml:"python" json:"python" mapstructure:"python"` - Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"` - FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"` - FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"` - FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"` - Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"` - Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` - Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` - Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` - Name string `yaml:"name" json:"name" mapstructure:"name"` - Source sourceCfg `yaml:"source" json:"source" mapstructure:"source"` - Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel - DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source - BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths - ExcludeBinaryOverlapByOwnership bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"` // exclude synthetic binary packages owned by os package files -} - -func (cfg Application) ToCatalogerConfig() cataloger.Config { - return cataloger.Config{ - Search: cataloger.SearchConfig{ - IncludeIndexedArchives: cfg.Package.SearchIndexedArchives, - IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives, - Scope: cfg.Package.Cataloger.ScopeOpt, - }, - Catalogers: cfg.Catalogers, - Parallelism: cfg.Parallelism, - ExcludeBinaryOverlapByOwnership: cfg.ExcludeBinaryOverlapByOwnership, - Golang: golangCataloger.NewGoCatalogerOpts(). - WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses). - WithLocalModCacheDir(cfg.Golang.LocalModCacheDir). - WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses). - WithProxy(cfg.Golang.Proxy). - WithNoProxy(cfg.Golang.NoProxy), - LinuxKernel: kernel.LinuxCatalogerConfig{ - CatalogModules: cfg.LinuxKernel.CatalogModules, - }, - Python: pythonCataloger.CatalogerConfig{ - GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements, - }, - } -} - -func (cfg *Application) LoadAllValues(v *viper.Viper, configPath string) error { - // priority order: viper.Set, flag, env, config, kv, defaults - // flags have already been loaded into viper by command construction - - // check if user specified config; otherwise read all possible paths - if err := loadConfig(v, configPath); err != nil { - var notFound *viper.ConfigFileNotFoundError - if errors.As(err, ¬Found) { - log.Debugf("no config file found, using defaults") - } else { - return fmt.Errorf("unable to load config: %w", err) - } - } - - // load default config values into viper - loadDefaultValues(v) - - // load environment variables - v.SetEnvPrefix(internal.ApplicationName) - v.AllowEmptyEnv(true) - v.AutomaticEnv() - - // unmarshal fully populated viper object onto config - err := v.Unmarshal(cfg) - if err != nil { - return err - } - - // Convert all populated config options to their internal application values ex: scope string => scopeOpt source.Scope - return cfg.parseConfigValues() -} - -func (cfg *Application) parseConfigValues() error { - // parse options on this struct - var catalogers []string - for _, c := range cfg.Catalogers { - for _, f := range strings.Split(c, ",") { - catalogers = append(catalogers, strings.TrimSpace(f)) - } - } - sort.Strings(catalogers) - cfg.Catalogers = catalogers - - // parse application config options - for _, optionFn := range []func() error{ - cfg.parseLogLevelOption, - cfg.parseFile, - } { - if err := optionFn(); err != nil { - return err - } - } - - if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil { - return err - } - - if cfg.Name != "" { - log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version") - if cfg.Source.Name == "" { - cfg.Source.Name = cfg.Name - } - } - - // check for valid default source options - // 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.Verbosity > 0: - cfg.Log.Level = logger.LevelFromVerbosity(int(cfg.Verbosity), 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) parseFile() error { - if cfg.File != "" { - expandedPath, err := homedir.Expand(cfg.File) - if err != nil { - return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err) - } - cfg.File = expandedPath - } - return nil -} - -// init loads the default configuration values into the viper instance (before the config values are read and parsed). -func loadDefaultValues(v *viper.Viper) { - // set the default values for primitive fields in this struct - v.SetDefault("quiet", false) - v.SetDefault("check-for-app-update", true) - v.SetDefault("catalogers", nil) - v.SetDefault("parallelism", 1) - v.SetDefault("default-image-pull-source", "") - v.SetDefault("exclude-binary-overlap-by-ownership", true) - - // for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does - value := reflect.ValueOf(Application{}) - 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) String() string { - // yaml is pretty human friendly (at least when compared to json) - appaStr, err := yaml.Marshal(&cfg) - - if err != nil { - return err.Error() - } - - return string(appaStr) -} - -// nolint:funlen -func loadConfig(v *viper.Viper, configPath string) error { - var err error - // 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) - } - v.Set("config", v.ConfigFileUsed()) - // 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) - confFilePath := "." + internal.ApplicationName - - // TODO: Remove this before v1.0.0 - // See syft #1634 - v.AddConfigPath(".") - v.SetConfigName(confFilePath) - - // check if config.yaml exists in the current directory - // DEPRECATED: this will be removed in v1.0.0 - if _, err := os.Stat("config.yaml"); err == nil { - log.Warn("DEPRECATED: ./config.yaml as a configuration file is deprecated and will be removed as an option in v1.0.0, please rename to .syft.yaml") - } - - if _, err := os.Stat(confFilePath + ".yaml"); err == nil { - if err = v.ReadInConfig(); err == nil { - v.Set("config", v.ConfigFileUsed()) - 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 { - v.Set("config", v.ConfigFileUsed()) - 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 { - v.Set("config", v.ConfigFileUsed()) - 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.SetConfigName("config") - configPath = path.Join(xdg.ConfigHome, internal.ApplicationName) - v.AddConfigPath(configPath) - for _, dir := range xdg.ConfigDirs { - v.AddConfigPath(path.Join(dir, internal.ApplicationName)) - } - if err = v.ReadInConfig(); err == nil { - v.Set("config", v.ConfigFileUsed()) - return nil - } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { - return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) - } - return nil -} - -var validDefaultSourceValues = []string{"registry", "docker", "podman", ""} - -func checkDefaultSourceValues(source string) error { - validValues := internal.NewStringSet(validDefaultSourceValues...) - if !validValues.Contains(source) { - validValuesString := strings.Join(validDefaultSourceValues, ", ") - return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString) - } - - return nil -} diff --git a/internal/config/application_test.go b/internal/config/application_test.go deleted file mode 100644 index eac0280de..000000000 --- a/internal/config/application_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path" - "testing" - - "github.com/adrg/xdg" - "github.com/mitchellh/go-homedir" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TODO: set negative case when config.yaml is no longer a valid option -func TestApplicationConfig(t *testing.T) { - // disable homedir package cache for testing - originalCacheOpt := homedir.DisableCache - homedir.DisableCache = true - t.Cleanup(func() { - homedir.DisableCache = originalCacheOpt - }) - - // ensure we have no side effects for xdg package for future tests - originalXDG := os.Getenv("XDG_CONFIG_HOME") - t.Cleanup(func() { - // note: we're not using t.Setenv since the effect we're trying to eliminate is within the xdg package - require.NoError(t, os.Setenv("XDG_CONFIG_HOME", originalXDG)) - xdg.Reload() - }) - - // config is picked up at desired configuration paths - // VALID: .syft.yaml, .syft/config.yaml, ~/.syft.yaml, /syft/config.yaml - // DEPRECATED: config.yaml is currently supported by - tests := []struct { - name string - setup func(t *testing.T) string - assertions func(t *testing.T, app *Application) - cleanup func() - }{ - { - name: "explicit config", - setup: func(t *testing.T) string { - return "./test-fixtures/.syft.yaml" - }, // no-op for explicit config - assertions: func(t *testing.T, app *Application) { - assert.Equal(t, "test-explicit-config", app.File) - }, - }, - { - name: "current working directory named config", - setup: func(t *testing.T) string { - err := os.Chdir("./test-fixtures/config-wd-file") // change application cwd to test-fixtures - require.NoError(t, err) - return "" - }, - assertions: func(t *testing.T, app *Application) { - assert.Equal(t, "test-wd-named-config", app.File) - }, - }, - { - name: "current working directory syft dir config", - setup: func(t *testing.T) string { - err := os.Chdir("./test-fixtures/config-dir-test") // change application cwd to test-fixtures - require.NoError(t, err) - return "" - }, - assertions: func(t *testing.T, app *Application) { - assert.Equal(t, "test-dir-config", app.File) - }, - }, - { - name: "home directory file config", - setup: func(t *testing.T) string { - // Because Setenv affects the whole process, it cannot be used in parallel tests or - // tests with parallel ancestors: see separate XDG test for consequence of this - t.Setenv("HOME", "./test-fixtures/config-home-test/config-file") - return "" - }, - assertions: func(t *testing.T, app *Application) { - assert.Equal(t, "test-home-config", app.File) - }, - }, - { - name: "XDG file config", - setup: func(t *testing.T) string { - wd, err := os.Getwd() - require.NoError(t, err) - configDir := path.Join(wd, "./test-fixtures/config-home-test") // set HOME to testdata - // note: this explicitly has multiple XDG paths, make certain we use the first VALID one (not the first one) - t.Setenv("XDG_CONFIG_DIRS", fmt.Sprintf("/another/foo/bar:%s", configDir)) - xdg.Reload() - return "" - }, - assertions: func(t *testing.T, app *Application) { - assert.Equal(t, "test-home-XDG-config", app.File) - }, - cleanup: func() { - require.NoError(t, os.Unsetenv("XDG_CONFIG_DIRS")) - xdg.Reload() - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.cleanup != nil { - t.Cleanup(test.cleanup) - } - wd, err := os.Getwd() - require.NoError(t, err) - - defer os.Chdir(wd) // reset working directory after test - application := &Application{} - viperInstance := viper.New() - - // this will override home in case you are running this test locally and DO have a syft config - // in your home directory... now it will be ignored. Same for XDG_CONFIG_DIRS. - t.Setenv("HOME", "/foo/bar") - t.Setenv("XDG_CONFIG_DIRS", "/foo/bar") - xdg.Reload() - - configPath := test.setup(t) - err = application.LoadAllValues(viperInstance, configPath) - require.NoError(t, err) - test.assertions(t, application) - }) - } -} diff --git a/internal/config/attest.go b/internal/config/attest.go deleted file mode 100644 index 659c7d3ed..000000000 --- a/internal/config/attest.go +++ /dev/null @@ -1,14 +0,0 @@ -package config - -import "github.com/spf13/viper" - -type attest struct { - // IMPORTANT: do not show the attestation key/password in any YAML/JSON output (sensitive information) - Key string `yaml:"-" json:"-" mapstructure:"key"` - Password string `yaml:"-" json:"-" mapstructure:"password"` -} - -func (cfg attest) loadDefaultValues(v *viper.Viper) { - v.SetDefault("attest.key", "") - v.SetDefault("attest.password", "") -} diff --git a/internal/config/cataloger_options.go b/internal/config/cataloger_options.go deleted file mode 100644 index 1fb2992b4..000000000 --- a/internal/config/cataloger_options.go +++ /dev/null @@ -1,29 +0,0 @@ -package config - -import ( - "fmt" - - "github.com/spf13/viper" - - "github.com/anchore/syft/syft/source" -) - -type catalogerOptions struct { - Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` - Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` - ScopeOpt source.Scope `yaml:"-" json:"-"` -} - -func (cfg catalogerOptions) loadDefaultValues(v *viper.Viper) { - v.SetDefault("package.cataloger.enabled", true) -} - -func (cfg *catalogerOptions) 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 -} diff --git a/internal/config/development.go b/internal/config/development.go deleted file mode 100644 index 4e1e8b01a..000000000 --- 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/file_classification.go b/internal/config/file_classification.go deleted file mode 100644 index d78812705..000000000 --- a/internal/config/file_classification.go +++ /dev/null @@ -1,20 +0,0 @@ -package config - -import ( - "github.com/spf13/viper" - - "github.com/anchore/syft/syft/source" -) - -type fileClassification struct { - Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` -} - -func (cfg fileClassification) loadDefaultValues(v *viper.Viper) { - v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault) - v.SetDefault("file-classification.cataloger.scope", source.SquashedScope) -} - -func (cfg *fileClassification) parseConfigValues() error { - return cfg.Cataloger.parseConfigValues() -} diff --git a/internal/config/file_contents.go b/internal/config/file_contents.go deleted file mode 100644 index 18a404f5b..000000000 --- a/internal/config/file_contents.go +++ /dev/null @@ -1,25 +0,0 @@ -package config - -import ( - "github.com/spf13/viper" - - "github.com/anchore/syft/internal/file" - "github.com/anchore/syft/syft/source" -) - -type fileContents struct { - Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` - SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"` - Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"` -} - -func (cfg fileContents) loadDefaultValues(v *viper.Viper) { - v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault) - v.SetDefault("file-contents.cataloger.scope", source.SquashedScope) - v.SetDefault("file-contents.skip-files-above-size", 1*file.MB) - v.SetDefault("file-contents.globs", []string{}) -} - -func (cfg *fileContents) parseConfigValues() error { - return cfg.Cataloger.parseConfigValues() -} diff --git a/internal/config/file_metadata.go b/internal/config/file_metadata.go deleted file mode 100644 index 5339f29d7..000000000 --- a/internal/config/file_metadata.go +++ /dev/null @@ -1,22 +0,0 @@ -package config - -import ( - "github.com/spf13/viper" - - "github.com/anchore/syft/syft/source" -) - -type FileMetadata struct { - Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` - Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"` -} - -func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) { - v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault) - v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope) - v.SetDefault("file-metadata.digests", []string{"sha256"}) -} - -func (cfg *FileMetadata) parseConfigValues() error { - return cfg.Cataloger.parseConfigValues() -} diff --git a/internal/config/linux_kernel.go b/internal/config/linux_kernel.go deleted file mode 100644 index 1d0b38e96..000000000 --- a/internal/config/linux_kernel.go +++ /dev/null @@ -1,11 +0,0 @@ -package config - -import "github.com/spf13/viper" - -type linuxKernel struct { - CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"` -} - -func (cfg linuxKernel) loadDefaultValues(v *viper.Viper) { - v.SetDefault("linux-kernel.catalog-modules", true) -} diff --git a/internal/config/logging.go b/internal/config/logging.go deleted file mode 100644 index 8c0b43c93..000000000 --- a/internal/config/logging.go +++ /dev/null @@ -1,34 +0,0 @@ -package config - -import ( - "fmt" - - "github.com/mitchellh/go-homedir" - "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-location" mapstructure:"file"` // the file path to write logs to -} - -func (cfg *logging) parseConfigValues() error { - if cfg.FileLocation != "" { - expandedPath, err := homedir.Expand(cfg.FileLocation) - if err != nil { - return fmt.Errorf("unable to expand log file path=%q: %w", cfg.FileLocation, err) - } - cfg.FileLocation = expandedPath - } - return nil -} - -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/pkg.go b/internal/config/pkg.go deleted file mode 100644 index 21e26a59b..000000000 --- a/internal/config/pkg.go +++ /dev/null @@ -1,24 +0,0 @@ -package config - -import ( - "github.com/spf13/viper" - - "github.com/anchore/syft/syft/pkg/cataloger" -) - -type pkg struct { - Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` - SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"` - SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"` -} - -func (cfg pkg) loadDefaultValues(v *viper.Viper) { - cfg.Cataloger.loadDefaultValues(v) - c := cataloger.DefaultSearchConfig() - v.SetDefault("package.search-unindexed-archives", c.IncludeUnindexedArchives) - v.SetDefault("package.search-indexed-archives", c.IncludeIndexedArchives) -} - -func (cfg *pkg) parseConfigValues() error { - return cfg.Cataloger.parseConfigValues() -} diff --git a/internal/config/registry.go b/internal/config/registry.go deleted file mode 100644 index 7e7e7132b..000000000 --- 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("SYFT_REGISTRY_AUTH_AUTHORITY"), - os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"), - os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"), - os.Getenv("SYFT_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/test-fixtures/.syft.yaml b/internal/config/test-fixtures/.syft.yaml deleted file mode 100644 index 9da17f95b..000000000 --- a/internal/config/test-fixtures/.syft.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# same as --file; write output report to a file (default is to write to stdout) -file: "test-explicit-config" -package: - cataloger: - scope: "squashed" - - # same as --scope; limit the scope of the cataloger to only the specified types \ No newline at end of file diff --git a/internal/config/test-fixtures/config-dir-test/.syft/config.yaml b/internal/config/test-fixtures/config-dir-test/.syft/config.yaml deleted file mode 100644 index 0122d78dc..000000000 --- a/internal/config/test-fixtures/config-dir-test/.syft/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# same as --file; write output report to a file (default is to write to stdout) -file: "test-dir-config" -package: - cataloger: - scope: "squashed" \ No newline at end of file diff --git a/internal/config/test-fixtures/config-home-test/config-file/.syft.yaml b/internal/config/test-fixtures/config-home-test/config-file/.syft.yaml deleted file mode 100644 index 04b886ba5..000000000 --- a/internal/config/test-fixtures/config-home-test/config-file/.syft.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# same as --file; write output report to a file (default is to write to stdout) -file: "test-home-config" -package: - cataloger: - scope: "squashed" \ No newline at end of file diff --git a/internal/config/test-fixtures/config-home-test/syft/config.yaml b/internal/config/test-fixtures/config-home-test/syft/config.yaml deleted file mode 100644 index 3c64be119..000000000 --- a/internal/config/test-fixtures/config-home-test/syft/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# same as --file; write output report to a file (default is to write to stdout) -file: "test-home-XDG-config" -package: - cataloger: - scope: "squashed" \ No newline at end of file diff --git a/internal/config/test-fixtures/config-wd-file/.syft.yaml b/internal/config/test-fixtures/config-wd-file/.syft.yaml deleted file mode 100644 index 6971cbee5..000000000 --- a/internal/config/test-fixtures/config-wd-file/.syft.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# same as --file; write output report to a file (default is to write to stdout) -file: "test-wd-named-config" -package: - cataloger: - scope: "squashed" \ No newline at end of file diff --git a/internal/constants.go b/internal/constants.go index 354a54274..0f7c9cc0f 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -1,10 +1,7 @@ package internal const ( - // ApplicationName is the non-capitalized name of the application (do not change this) - ApplicationName = "syft" - // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "10.0.0" + JSONSchemaVersion = "11.0.1" ) diff --git a/internal/log/log.go b/internal/log/log.go index 242c5f0b7..5c557ee0d 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -7,29 +7,28 @@ import ( "github.com/anchore/go-logger" "github.com/anchore/go-logger/adapter/discard" "github.com/anchore/go-logger/adapter/redact" + red "github.com/anchore/syft/internal/redact" ) -var ( - // log is the singleton used to facilitate logging internally within - log = discard.New() - - store = redact.NewStore() - - Redactor = store.(redact.Redactor) -) +// log is the singleton used to facilitate logging internally within +var log = discard.New() func Set(l logger.Logger) { - log = redact.New(l, store) + // 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 } -func Redact(values ...string) { - store.Add(values...) -} - // Errorf takes a formatted template string and template arguments for the error logging level. func Errorf(format string, args ...interface{}) { log.Errorf(format, args...) diff --git a/internal/redact/redact.go b/internal/redact/redact.go new file mode 100644 index 000000000..3bb76e269 --- /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/spdxlicense/license_list.go b/internal/spdxlicense/license_list.go index 8ffa768c1..d7fa6645e 100644 --- a/internal/spdxlicense/license_list.go +++ b/internal/spdxlicense/license_list.go @@ -1,9 +1,9 @@ // Code generated by go generate; DO NOT EDIT. -// This file was generated by robots at 2023-07-11 11:55:35.533815 -0400 EDT m=+0.695308614 +// This file was generated by robots at 2023-10-05 22:45:16.827263 -0400 EDT m=+0.138031521 // using data from https://spdx.org/licenses/licenses.json package spdxlicense -const Version = "3.21" +const Version = "3.22" var licenseIDs = map[string]string{ "0bsd": "0BSD", @@ -14,6 +14,7 @@ var licenseIDs = map[string]string{ "adobe2006.0": "Adobe-2006", "adobe2006.0.0": "Adobe-2006", "adobeglyph": "Adobe-Glyph", + "adobeutopia": "Adobe-Utopia", "adsl": "ADSL", "afl1": "AFL-1.1", "afl1.1": "AFL-1.1", @@ -145,6 +146,8 @@ var licenseIDs = map[string]string{ "bsd3.0.0clause": "BSD-3-Clause", "bsd3.0.0clauseattribution": "BSD-3-Clause-Attribution", "bsd3.0.0clauseclear": "BSD-3-Clause-Clear", + "bsd3.0.0clauseflex": "BSD-3-Clause-flex", + "bsd3.0.0clausehp": "BSD-3-Clause-HP", "bsd3.0.0clauselbnl": "BSD-3-Clause-LBNL", "bsd3.0.0clausemodification": "BSD-3-Clause-Modification", "bsd3.0.0clausenomilitarylicense": "BSD-3-Clause-No-Military-License", @@ -152,9 +155,12 @@ var licenseIDs = map[string]string{ "bsd3.0.0clausenonuclearlicense2014": "BSD-3-Clause-No-Nuclear-License-2014", "bsd3.0.0clausenonuclearwarranty": "BSD-3-Clause-No-Nuclear-Warranty", "bsd3.0.0clauseopenmpi": "BSD-3-Clause-Open-MPI", + "bsd3.0.0clausesun": "BSD-3-Clause-Sun", "bsd3.0clause": "BSD-3-Clause", "bsd3.0clauseattribution": "BSD-3-Clause-Attribution", "bsd3.0clauseclear": "BSD-3-Clause-Clear", + "bsd3.0clauseflex": "BSD-3-Clause-flex", + "bsd3.0clausehp": "BSD-3-Clause-HP", "bsd3.0clauselbnl": "BSD-3-Clause-LBNL", "bsd3.0clausemodification": "BSD-3-Clause-Modification", "bsd3.0clausenomilitarylicense": "BSD-3-Clause-No-Military-License", @@ -162,9 +168,12 @@ var licenseIDs = map[string]string{ "bsd3.0clausenonuclearlicense2014": "BSD-3-Clause-No-Nuclear-License-2014", "bsd3.0clausenonuclearwarranty": "BSD-3-Clause-No-Nuclear-Warranty", "bsd3.0clauseopenmpi": "BSD-3-Clause-Open-MPI", + "bsd3.0clausesun": "BSD-3-Clause-Sun", "bsd3clause": "BSD-3-Clause", "bsd3clauseattribution": "BSD-3-Clause-Attribution", "bsd3clauseclear": "BSD-3-Clause-Clear", + "bsd3clauseflex": "BSD-3-Clause-flex", + "bsd3clausehp": "BSD-3-Clause-HP", "bsd3clauselbnl": "BSD-3-Clause-LBNL", "bsd3clausemodification": "BSD-3-Clause-Modification", "bsd3clausenomilitarylicense": "BSD-3-Clause-No-Military-License", @@ -172,6 +181,7 @@ var licenseIDs = map[string]string{ "bsd3clausenonuclearlicense2014": "BSD-3-Clause-No-Nuclear-License-2014", "bsd3clausenonuclearwarranty": "BSD-3-Clause-No-Nuclear-Warranty", "bsd3clauseopenmpi": "BSD-3-Clause-Open-MPI", + "bsd3clausesun": "BSD-3-Clause-Sun", "bsd4.0.0clause": "BSD-4-Clause", "bsd4.0.0clauseshortened": "BSD-4-Clause-Shortened", "bsd4.0.0clauseuc": "BSD-4-Clause-UC", @@ -189,8 +199,10 @@ var licenseIDs = map[string]string{ "bsd4tahoe": "BSD-4.3TAHOE", "bsdadvertisingacknowledgement": "BSD-Advertising-Acknowledgement", "bsdattributionhpnddisclaimer": "BSD-Attribution-HPND-disclaimer", + "bsdinfernonettverk": "BSD-Inferno-Nettverk", "bsdprotection": "BSD-Protection", "bsdsourcecode": "BSD-Source-Code", + "bsdsystemics": "BSD-Systemics", "bsl1": "BSL-1.0", "bsl1.0": "BSL-1.0", "bsl1.0.0": "BSL-1.0", @@ -403,6 +415,7 @@ var licenseIDs = map[string]string{ "cernohlw2.0": "CERN-OHL-W-2.0", "cernohlw2.0.0": "CERN-OHL-W-2.0", "cfitsio": "CFITSIO", + "checkcvs": "check-cvs", "checkmk": "checkmk", "clartistic": "ClArtistic", "clips": "Clips", @@ -432,6 +445,7 @@ var licenseIDs = map[string]string{ "cpol1": "CPOL-1.02", "cpol1.02": "CPOL-1.02", "cpol1.02.0": "CPOL-1.02", + "cronyx": "Cronyx", "crossword": "Crossword", "crystalstacker": "CrystalStacker", "cuaopl1": "CUA-OPL-1.0", @@ -449,6 +463,9 @@ var licenseIDs = map[string]string{ "dldeby2": "DL-DE-BY-2.0", "dldeby2.0": "DL-DE-BY-2.0", "dldeby2.0.0": "DL-DE-BY-2.0", + "dldezero2": "DL-DE-ZERO-2.0", + "dldezero2.0": "DL-DE-ZERO-2.0", + "dldezero2.0.0": "DL-DE-ZERO-2.0", "doc": "DOC", "dotseqn": "Dotseqn", "drl1": "DRL-1.0", @@ -500,7 +517,9 @@ var licenseIDs = map[string]string{ "eupl1.2.0": "EUPL-1.2", "eurosym": "Eurosym", "fair": "Fair", + "fbm": "FBM", "fdkaac": "FDK-AAC", + "fergusontwofish": "Ferguson-Twofish", "frameworx1": "Frameworx-1.0", "frameworx1.0": "Frameworx-1.0", "frameworx1.0.0": "Frameworx-1.0", @@ -511,6 +530,8 @@ var licenseIDs = map[string]string{ "fsfullr": "FSFULLR", "fsfullrwd": "FSFULLRWD", "ftl": "FTL", + "furuseth": "Furuseth", + "fwlw": "fwlw", "gd": "GD", "gfdl1": "GFDL-1.1-only", "gfdl1+": "GFDL-1.1-or-later", @@ -658,11 +679,21 @@ var licenseIDs = map[string]string{ "hp1986": "HP-1986", "hp1986.0": "HP-1986", "hp1986.0.0": "HP-1986", + "hp1989": "HP-1989", + "hp1989.0": "HP-1989", + "hp1989.0.0": "HP-1989", "hpnd": "HPND", + "hpnddec": "HPND-DEC", + "hpnddoc": "HPND-doc", + "hpnddocsell": "HPND-doc-sell", "hpndexportus": "HPND-export-US", + "hpndexportusmodify": "HPND-export-US-modify", "hpndmarkuskuhn": "HPND-Markus-Kuhn", + "hpndpbmplus": "HPND-Pbmplus", + "hpndsellregexpr": "HPND-sell-regexpr", "hpndsellvariant": "HPND-sell-variant", "hpndsellvariantmitdisclaimer": "HPND-sell-variant-MIT-disclaimer", + "hpnduc": "HPND-UC", "htmltidy": "HTMLTIDY", "ibmpibs": "IBM-pibs", "icu": "ICU", @@ -695,6 +726,7 @@ var licenseIDs = map[string]string{ "jplimage": "JPL-image", "jpnic": "JPNIC", "json": "JSON", + "kastrup": "Kastrup", "kazlib": "Kazlib", "knuthctan": "Knuth-CTAN", "lal1": "LAL-1.2", @@ -788,14 +820,18 @@ var licenseIDs = map[string]string{ "lppl1.3c": "LPPL-1.3c", "lppl1a": "LPPL-1.3a", "lppl1c": "LPPL-1.3c", + "lsof": "lsof", + "lucidabitmapfonts": "Lucida-Bitmap-Fonts", "lzmasdk9": "LZMA-SDK-9.22", "lzmasdk9.11.0to9.20": "LZMA-SDK-9.11-to-9.20", "lzmasdk9.11to9.20": "LZMA-SDK-9.11-to-9.20", "lzmasdk9.22": "LZMA-SDK-9.22", "lzmasdk9.22.0": "LZMA-SDK-9.22", "lzmasdk9to9.20": "LZMA-SDK-9.11-to-9.20", + "magaz": "magaz", "makeindex": "MakeIndex", "martinbirgmeier": "Martin-Birgmeier", + "mcpheeslideshow": "McPhee-slideshow", "metamail": "metamail", "minpack": "Minpack", "miros": "MirOS", @@ -809,8 +845,11 @@ var licenseIDs = map[string]string{ "mitmodernvariant": "MIT-Modern-Variant", "mitnfa": "MITNFA", "mitopengroup": "MIT-open-group", + "mittestregex": "MIT-testregex", "mitwu": "MIT-Wu", + "mmixware": "MMIXware", "motosoto": "Motosoto", + "mpegssg": "MPEG-SSG", "mpich2": "mpich2", "mpich2.0": "mpich2", "mpich2.0.0": "mpich2", @@ -997,6 +1036,7 @@ var licenseIDs = map[string]string{ "ouda1": "O-UDA-1.0", "ouda1.0": "O-UDA-1.0", "ouda1.0.0": "O-UDA-1.0", + "padl": "PADL", "parity6": "Parity-6.0.0", "parity6.0": "Parity-6.0.0", "parity6.0.0": "Parity-6.0.0", @@ -1012,6 +1052,7 @@ var licenseIDs = map[string]string{ "php3.01": "PHP-3.01", "php3.01.0": "PHP-3.01", "plexus": "Plexus", + "pnmstitch": "pnmstitch", "polyformnoncommercial1": "PolyForm-Noncommercial-1.0.0", "polyformnoncommercial1.0": "PolyForm-Noncommercial-1.0.0", "polyformnoncommercial1.0.0": "PolyForm-Noncommercial-1.0.0", @@ -1028,6 +1069,7 @@ var licenseIDs = map[string]string{ "python2.0": "Python-2.0", "python2.0.0": "Python-2.0", "python2.0.1": "Python-2.0.1", + "pythonldap": "python-ldap", "qhull": "Qhull", "qpl1": "QPL-1.0", "qpl1.0": "QPL-1.0", @@ -1066,6 +1108,7 @@ var licenseIDs = map[string]string{ "sgib2": "SGI-B-2.0", "sgib2.0": "SGI-B-2.0", "sgib2.0.0": "SGI-B-2.0", + "sgiopengl": "SGI-OpenGL", "sgp4": "SGP4", "sgp4.0": "SGP4", "sgp4.0.0": "SGP4", @@ -1080,11 +1123,13 @@ var licenseIDs = map[string]string{ "sissl1": "SISSL-1.2", "sissl1.2": "SISSL-1.2", "sissl1.2.0": "SISSL-1.2", + "sl": "SL", "sleepycat": "Sleepycat", "smlnj": "SMLNJ", "smppl": "SMPPL", "snia": "SNIA", "snprintf": "snprintf", + "soundex": "Soundex", "spencer86": "Spencer-86", "spencer86.0": "Spencer-86", "spencer86.0.0": "Spencer-86", @@ -1097,6 +1142,7 @@ var licenseIDs = map[string]string{ "spl1": "SPL-1.0", "spl1.0": "SPL-1.0", "spl1.0.0": "SPL-1.0", + "sshkeyscan": "ssh-keyscan", "sshopenssh": "SSH-OpenSSH", "sshshort": "SSH-short", "sspl1": "SSPL-1.0", @@ -1108,6 +1154,7 @@ var licenseIDs = map[string]string{ "sugarcrm1.1.3": "SugarCRM-1.1.3", "sunpro": "SunPro", "swl": "SWL", + "swrule": "swrule", "symlinks": "Symlinks", "taprohl1": "TAPR-OHL-1.0", "taprohl1.0": "TAPR-OHL-1.0", @@ -1125,6 +1172,7 @@ var licenseIDs = map[string]string{ "tpl1.0": "TPL-1.0", "tpl1.0.0": "TPL-1.0", "ttwl": "TTWL", + "ttyp0": "TTYP0", "tuberlin1": "TU-Berlin-1.0", "tuberlin1.0": "TU-Berlin-1.0", "tuberlin1.0.0": "TU-Berlin-1.0", @@ -1135,6 +1183,7 @@ var licenseIDs = map[string]string{ "ucl1": "UCL-1.0", "ucl1.0": "UCL-1.0", "ucl1.0.0": "UCL-1.0", + "ulem": "ulem", "unicodedfs2015": "Unicode-DFS-2015", "unicodedfs2015.0": "Unicode-DFS-2015", "unicodedfs2015.0.0": "Unicode-DFS-2015", @@ -1147,6 +1196,7 @@ var licenseIDs = map[string]string{ "upl1": "UPL-1.0", "upl1.0": "UPL-1.0", "upl1.0.0": "UPL-1.0", + "urtrle": "URT-RLE", "vim": "Vim", "vostrom": "VOSTROM", "vsl1": "VSL-1.0", @@ -1196,6 +1246,7 @@ var licenseIDs = map[string]string{ "ypl1.1": "YPL-1.1", "ypl1.1.0": "YPL-1.1", "zed": "Zed", + "zeeff": "Zeeff", "zend2": "Zend-2.0", "zend2.0": "Zend-2.0", "zend2.0.0": "Zend-2.0", diff --git a/internal/version/build.go b/internal/version/build.go deleted file mode 100644 index a0d5fc1d2..000000000 --- a/internal/version/build.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Package version contains all build time metadata (version, build time, git commit, etc). -*/ -package version - -import ( - "fmt" - "runtime" - "strings" - - "github.com/anchore/syft/internal" -) - -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 - JSONSchemaVersion string `json:"jsonSchemaVersion"` // application semantic JSON schema version - 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 { - return Version{ - Version: version, - JSONSchemaVersion: internal.JSONSchemaVersion, - GitCommit: gitCommit, - GitDescription: gitDescription, - BuildDate: buildDate, - GoVersion: runtime.Version(), - Compiler: runtime.Compiler, - Platform: platform, - } -} diff --git a/internal/version/update.go b/internal/version/update.go deleted file mode 100644 index 00141d142..000000000 --- a/internal/version/update.go +++ /dev/null @@ -1,73 +0,0 @@ -package version - -import ( - "fmt" - "io" - "net/http" - "strings" - - hashiVersion "github.com/anchore/go-version" - "github.com/anchore/syft/internal" -) - -var latestAppVersionURL = struct { - host string - path string -}{ - host: "https://toolbox-data.anchore.io", - path: fmt.Sprintf("/%s/releases/latest/VERSION", internal.ApplicationName), -} - -// IsUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is. -func IsUpdateAvailable() (bool, string, error) { - currentBuildInfo := FromBuild() - if !currentBuildInfo.IsProductionBuild() { - // don't allow for non-production builds to check for a version. - return false, "", nil - } - currentVersion, err := hashiVersion.NewVersion(currentBuildInfo.Version) - if err != nil { - return false, "", fmt.Errorf("failed to parse current application version: %w", err) - } - - latestVersion, err := fetchLatestApplicationVersion() - if err != nil { - return false, "", err - } - - if latestVersion.GreaterThan(currentVersion) { - return true, latestVersion.String(), nil - } - - return false, "", nil -} - -func fetchLatestApplicationVersion() (*hashiVersion.Version, error) { - req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request for latest version: %w", err) - } - - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to fetch latest version: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status) - } - - versionBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read latest version: %w", err) - } - - versionStr := strings.TrimSuffix(string(versionBytes), "\n") - if len(versionStr) > 50 { - return nil, fmt.Errorf("version too long: %q", versionStr[:50]) - } - - return hashiVersion.NewVersion(versionStr) -} diff --git a/schema/cyclonedx/README.md b/schema/cyclonedx/README.md index 200d6393e..b9278db66 100644 --- a/schema/cyclonedx/README.md +++ b/schema/cyclonedx/README.md @@ -1,6 +1,6 @@ # CycloneDX Schemas -`syft` generates a CycloneDX BOm output. We want to be able to validate the CycloneDX schemas +`syft` generates a CycloneDX Bom output. We want to be able to validate the CycloneDX schemas (and dependent schemas) against generated syft output. The best way to do this is with `xmllint`, however, this tool does not know how to deal with references from HTTP, only the local filesystem. For this reason we've included a copy of all schemas needed to validate `syft` output, modified diff --git a/schema/json/schema-10.0.1.json b/schema/json/schema-10.0.1.json new file mode 100644 index 000000000..730db95eb --- /dev/null +++ b/schema/json/schema-10.0.1.json @@ -0,0 +1,1929 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/10.0.1/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "gid": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "size": { + "type": "string" + }, + "link": { + "type": "string" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object" + }, + "AlpmMetadata": { + "properties": { + "basepackage": { + "type": "string" + }, + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "packager": { + "type": "string" + }, + "url": { + "type": "string" + }, + "validation": { + "type": "string" + }, + "reason": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ] + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "ApkMetadata": { + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "installedSize": { + "type": "integer" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ] + }, + "BinaryMetadata": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ] + }, + "CargoPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ] + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ] + }, + "CocoapodsMetadata": { + "properties": { + "checksum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "checksum" + ] + }, + "ConanLockMetadata": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "requires": { + "type": "string" + }, + "build_requires": { + "type": "string" + }, + "py_requires": { + "type": "string" + }, + "options": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "path": { + "type": "string" + }, + "context": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "ConanMetadata": { + "properties": { + "ref": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "Coordinates": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "DartPubMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hosted_url": { + "type": "string" + }, + "vcs_url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Descriptor": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": true + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array" + }, + "secrets": { + "items": { + "$ref": "#/$defs/Secrets" + }, + "type": "array" + }, + "source": { + "$ref": "#/$defs/Source" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor" + }, + "schema": { + "$ref": "#/$defs/Schema" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ] + }, + "DotnetDepsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sha512": { + "type": "string" + }, + "hashPath": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ] + }, + "DotnetPortableExecutableMetadata": { + "properties": { + "assemblyVersion": { + "type": "string" + }, + "legalCopyright": { + "type": "string" + }, + "comments": { + "type": "string" + }, + "internalName": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "productVersion": { + "type": "string" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ] + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ] + }, + "DpkgMetadata": { + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ] + }, + "File": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Coordinates" + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "id", + "location" + ] + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ] + }, + "GemMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "GolangBinMetadata": { + "properties": { + "goBuildSettings": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + }, + "mainModule": { + "type": "string" + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ] + }, + "GolangModMetadata": { + "properties": { + "h1Digest": { + "type": "string" + } + }, + "type": "object" + }, + "HackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "snapshotURL": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "JavaManifest": { + "properties": { + "main": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "namedSections": { + "patternProperties": { + ".*": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "JavaMetadata": { + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest" + }, + "pomProperties": { + "$ref": "#/$defs/PomProperties" + }, + "pomProject": { + "$ref": "#/$defs/PomProject" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "virtualPath" + ] + }, + "KbPackageMetadata": { + "properties": { + "product_id": { + "type": "string" + }, + "kb": { + "type": "string" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ] + }, + "License": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ] + }, + "LinuxKernelMetadata": { + "properties": { + "name": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extendedVersion": { + "type": "string" + }, + "buildTime": { + "type": "string" + }, + "author": { + "type": "string" + }, + "format": { + "type": "string" + }, + "rwRootFS": { + "type": "boolean" + }, + "swapDevice": { + "type": "integer" + }, + "rootDevice": { + "type": "integer" + }, + "videoMode": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ] + }, + "LinuxKernelModuleMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "license": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "versionMagic": { + "type": "string" + }, + "parameters": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "$ref": "#/$defs/IDLikes" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "versionCodename": { + "type": "string" + }, + "buildID": { + "type": "string" + }, + "imageID": { + "type": "string" + }, + "imageVersion": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + }, + "supportEnd": { + "type": "string" + } + }, + "type": "object" + }, + "Location": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + }, + "annotations": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "MixLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "NixStoreMetadata": { + "properties": { + "outputHash": { + "type": "string" + }, + "output": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "outputHash", + "files" + ] + }, + "NpmPackageJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "private": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ] + }, + "NpmPackageLockJSONMetadata": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmMetadata" + }, + { + "$ref": "#/$defs/ApkMetadata" + }, + { + "$ref": "#/$defs/BinaryMetadata" + }, + { + "$ref": "#/$defs/CargoPackageMetadata" + }, + { + "$ref": "#/$defs/CocoapodsMetadata" + }, + { + "$ref": "#/$defs/ConanLockMetadata" + }, + { + "$ref": "#/$defs/ConanMetadata" + }, + { + "$ref": "#/$defs/DartPubMetadata" + }, + { + "$ref": "#/$defs/DotnetDepsMetadata" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableMetadata" + }, + { + "$ref": "#/$defs/DpkgMetadata" + }, + { + "$ref": "#/$defs/GemMetadata" + }, + { + "$ref": "#/$defs/GolangBinMetadata" + }, + { + "$ref": "#/$defs/GolangModMetadata" + }, + { + "$ref": "#/$defs/HackageMetadata" + }, + { + "$ref": "#/$defs/JavaMetadata" + }, + { + "$ref": "#/$defs/KbPackageMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelModuleMetadata" + }, + { + "$ref": "#/$defs/MixLockMetadata" + }, + { + "$ref": "#/$defs/NixStoreMetadata" + }, + { + "$ref": "#/$defs/NpmPackageJSONMetadata" + }, + { + "$ref": "#/$defs/NpmPackageLockJSONMetadata" + }, + { + "$ref": "#/$defs/PhpComposerJSONMetadata" + }, + { + "$ref": "#/$defs/PortageMetadata" + }, + { + "$ref": "#/$defs/PythonPackageMetadata" + }, + { + "$ref": "#/$defs/PythonPipfileLockMetadata" + }, + { + "$ref": "#/$defs/PythonRequirementsMetadata" + }, + { + "$ref": "#/$defs/RDescriptionFileMetadata" + }, + { + "$ref": "#/$defs/RebarLockMetadata" + }, + { + "$ref": "#/$defs/RpmMetadata" + }, + { + "$ref": "#/$defs/SwiftPackageManagerMetadata" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ] + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name" + ] + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ] + }, + "PhpComposerJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ] + }, + "PomParent": { + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ] + }, + "PomProject": { + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$ref": "#/$defs/PomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ] + }, + "PomProperties": { + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ] + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PortageMetadata": { + "properties": { + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ] + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "type": "object", + "required": [ + "url" + ] + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PythonPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ] + }, + "PythonPipfileLockMetadata": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "index": { + "type": "string" + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ] + }, + "PythonRequirementsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array" + }, + "versionConstraint": { + "type": "string" + }, + "url": { + "type": "string" + }, + "markers": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ] + }, + "RDescriptionFileMetadata": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "repository": { + "type": "string" + }, + "built": { + "type": "string" + }, + "needsCompilation": { + "type": "boolean" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "RebarLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "Relationship": { + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ] + }, + "RpmMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmdbFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "modularityLabel", + "files" + ] + }, + "RpmdbFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ] + }, + "Schema": { + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "version", + "url" + ] + }, + "SearchResult": { + "properties": { + "classification": { + "type": "string" + }, + "lineNumber": { + "type": "integer" + }, + "lineOffset": { + "type": "integer" + }, + "seekPosition": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "classification", + "lineNumber", + "lineOffset", + "seekPosition", + "length" + ] + }, + "Secrets": { + "properties": { + "location": { + "$ref": "#/$defs/Coordinates" + }, + "secrets": { + "items": { + "$ref": "#/$defs/SearchResult" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "location", + "secrets" + ] + }, + "Source": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ] + }, + "SwiftPackageManagerMetadata": { + "properties": { + "revision": { + "type": "string" + } + }, + "type": "object", + "required": [ + "revision" + ] + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-10.0.2.json b/schema/json/schema-10.0.2.json new file mode 100644 index 000000000..d28a36bc0 --- /dev/null +++ b/schema/json/schema-10.0.2.json @@ -0,0 +1,1976 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/10.0.2/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "gid": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "size": { + "type": "string" + }, + "link": { + "type": "string" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object" + }, + "AlpmMetadata": { + "properties": { + "basepackage": { + "type": "string" + }, + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "packager": { + "type": "string" + }, + "url": { + "type": "string" + }, + "validation": { + "type": "string" + }, + "reason": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ] + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "ApkMetadata": { + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "installedSize": { + "type": "integer" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ] + }, + "BinaryMetadata": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ] + }, + "CargoPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ] + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ] + }, + "CocoapodsMetadata": { + "properties": { + "checksum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "checksum" + ] + }, + "ConanLockMetadata": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "requires": { + "type": "string" + }, + "build_requires": { + "type": "string" + }, + "py_requires": { + "type": "string" + }, + "options": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "path": { + "type": "string" + }, + "context": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "ConanMetadata": { + "properties": { + "ref": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "Coordinates": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "DartPubMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hosted_url": { + "type": "string" + }, + "vcs_url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Descriptor": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": true + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array" + }, + "secrets": { + "items": { + "$ref": "#/$defs/Secrets" + }, + "type": "array" + }, + "source": { + "$ref": "#/$defs/Source" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor" + }, + "schema": { + "$ref": "#/$defs/Schema" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ] + }, + "DotnetDepsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sha512": { + "type": "string" + }, + "hashPath": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ] + }, + "DotnetPortableExecutableMetadata": { + "properties": { + "assemblyVersion": { + "type": "string" + }, + "legalCopyright": { + "type": "string" + }, + "comments": { + "type": "string" + }, + "internalName": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "productVersion": { + "type": "string" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ] + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ] + }, + "DpkgMetadata": { + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ] + }, + "File": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Coordinates" + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "id", + "location" + ] + }, + "FileLicense": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ] + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "extent": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ] + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ] + }, + "GemMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "GolangBinMetadata": { + "properties": { + "goBuildSettings": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + }, + "mainModule": { + "type": "string" + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ] + }, + "GolangModMetadata": { + "properties": { + "h1Digest": { + "type": "string" + } + }, + "type": "object" + }, + "HackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "snapshotURL": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "JavaManifest": { + "properties": { + "main": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "namedSections": { + "patternProperties": { + ".*": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "JavaMetadata": { + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest" + }, + "pomProperties": { + "$ref": "#/$defs/PomProperties" + }, + "pomProject": { + "$ref": "#/$defs/PomProject" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "virtualPath" + ] + }, + "KbPackageMetadata": { + "properties": { + "product_id": { + "type": "string" + }, + "kb": { + "type": "string" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ] + }, + "License": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ] + }, + "LinuxKernelMetadata": { + "properties": { + "name": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extendedVersion": { + "type": "string" + }, + "buildTime": { + "type": "string" + }, + "author": { + "type": "string" + }, + "format": { + "type": "string" + }, + "rwRootFS": { + "type": "boolean" + }, + "swapDevice": { + "type": "integer" + }, + "rootDevice": { + "type": "integer" + }, + "videoMode": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ] + }, + "LinuxKernelModuleMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "license": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "versionMagic": { + "type": "string" + }, + "parameters": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "$ref": "#/$defs/IDLikes" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "versionCodename": { + "type": "string" + }, + "buildID": { + "type": "string" + }, + "imageID": { + "type": "string" + }, + "imageVersion": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + }, + "supportEnd": { + "type": "string" + } + }, + "type": "object" + }, + "Location": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + }, + "annotations": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "MixLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "NixStoreMetadata": { + "properties": { + "outputHash": { + "type": "string" + }, + "output": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "outputHash", + "files" + ] + }, + "NpmPackageJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "private": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ] + }, + "NpmPackageLockJSONMetadata": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmMetadata" + }, + { + "$ref": "#/$defs/ApkMetadata" + }, + { + "$ref": "#/$defs/BinaryMetadata" + }, + { + "$ref": "#/$defs/CargoPackageMetadata" + }, + { + "$ref": "#/$defs/CocoapodsMetadata" + }, + { + "$ref": "#/$defs/ConanLockMetadata" + }, + { + "$ref": "#/$defs/ConanMetadata" + }, + { + "$ref": "#/$defs/DartPubMetadata" + }, + { + "$ref": "#/$defs/DotnetDepsMetadata" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableMetadata" + }, + { + "$ref": "#/$defs/DpkgMetadata" + }, + { + "$ref": "#/$defs/GemMetadata" + }, + { + "$ref": "#/$defs/GolangBinMetadata" + }, + { + "$ref": "#/$defs/GolangModMetadata" + }, + { + "$ref": "#/$defs/HackageMetadata" + }, + { + "$ref": "#/$defs/JavaMetadata" + }, + { + "$ref": "#/$defs/KbPackageMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelModuleMetadata" + }, + { + "$ref": "#/$defs/MixLockMetadata" + }, + { + "$ref": "#/$defs/NixStoreMetadata" + }, + { + "$ref": "#/$defs/NpmPackageJSONMetadata" + }, + { + "$ref": "#/$defs/NpmPackageLockJSONMetadata" + }, + { + "$ref": "#/$defs/PhpComposerJSONMetadata" + }, + { + "$ref": "#/$defs/PortageMetadata" + }, + { + "$ref": "#/$defs/PythonPackageMetadata" + }, + { + "$ref": "#/$defs/PythonPipfileLockMetadata" + }, + { + "$ref": "#/$defs/PythonRequirementsMetadata" + }, + { + "$ref": "#/$defs/RDescriptionFileMetadata" + }, + { + "$ref": "#/$defs/RebarLockMetadata" + }, + { + "$ref": "#/$defs/RpmMetadata" + }, + { + "$ref": "#/$defs/SwiftPackageManagerMetadata" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ] + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name" + ] + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ] + }, + "PhpComposerJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ] + }, + "PomParent": { + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ] + }, + "PomProject": { + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$ref": "#/$defs/PomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ] + }, + "PomProperties": { + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ] + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PortageMetadata": { + "properties": { + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ] + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "type": "object", + "required": [ + "url" + ] + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PythonPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ] + }, + "PythonPipfileLockMetadata": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "index": { + "type": "string" + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ] + }, + "PythonRequirementsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array" + }, + "versionConstraint": { + "type": "string" + }, + "url": { + "type": "string" + }, + "markers": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ] + }, + "RDescriptionFileMetadata": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "repository": { + "type": "string" + }, + "built": { + "type": "string" + }, + "needsCompilation": { + "type": "boolean" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "RebarLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "Relationship": { + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ] + }, + "RpmMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmdbFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "modularityLabel", + "files" + ] + }, + "RpmdbFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ] + }, + "Schema": { + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "version", + "url" + ] + }, + "SearchResult": { + "properties": { + "classification": { + "type": "string" + }, + "lineNumber": { + "type": "integer" + }, + "lineOffset": { + "type": "integer" + }, + "seekPosition": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "classification", + "lineNumber", + "lineOffset", + "seekPosition", + "length" + ] + }, + "Secrets": { + "properties": { + "location": { + "$ref": "#/$defs/Coordinates" + }, + "secrets": { + "items": { + "$ref": "#/$defs/SearchResult" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "location", + "secrets" + ] + }, + "Source": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ] + }, + "SwiftPackageManagerMetadata": { + "properties": { + "revision": { + "type": "string" + } + }, + "type": "object", + "required": [ + "revision" + ] + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-11.0.0.json b/schema/json/schema-11.0.0.json new file mode 100644 index 000000000..a36403387 --- /dev/null +++ b/schema/json/schema-11.0.0.json @@ -0,0 +1,1985 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/11.0.0/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "gid": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "size": { + "type": "string" + }, + "link": { + "type": "string" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object" + }, + "AlpmMetadata": { + "properties": { + "basepackage": { + "type": "string" + }, + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "packager": { + "type": "string" + }, + "url": { + "type": "string" + }, + "validation": { + "type": "string" + }, + "reason": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ] + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "ApkMetadata": { + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "installedSize": { + "type": "integer" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ] + }, + "BinaryMetadata": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ] + }, + "CargoPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ] + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ] + }, + "CocoapodsMetadata": { + "properties": { + "checksum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "checksum" + ] + }, + "ConanLockMetadata": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "options": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "path": { + "type": "string" + }, + "context": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "ConanMetadata": { + "properties": { + "ref": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "Coordinates": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "DartPubMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hosted_url": { + "type": "string" + }, + "vcs_url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Descriptor": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": true + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array" + }, + "secrets": { + "items": { + "$ref": "#/$defs/Secrets" + }, + "type": "array" + }, + "source": { + "$ref": "#/$defs/Source" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor" + }, + "schema": { + "$ref": "#/$defs/Schema" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ] + }, + "DotnetDepsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sha512": { + "type": "string" + }, + "hashPath": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ] + }, + "DotnetPortableExecutableMetadata": { + "properties": { + "assemblyVersion": { + "type": "string" + }, + "legalCopyright": { + "type": "string" + }, + "comments": { + "type": "string" + }, + "internalName": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "productVersion": { + "type": "string" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ] + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ] + }, + "DpkgMetadata": { + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ] + }, + "File": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Coordinates" + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "id", + "location" + ] + }, + "FileLicense": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ] + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "extent": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ] + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ] + }, + "GemMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "GolangBinMetadata": { + "properties": { + "goBuildSettings": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + }, + "mainModule": { + "type": "string" + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ] + }, + "GolangModMetadata": { + "properties": { + "h1Digest": { + "type": "string" + } + }, + "type": "object" + }, + "HackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "snapshotURL": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "JavaManifest": { + "properties": { + "main": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "namedSections": { + "patternProperties": { + ".*": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "JavaMetadata": { + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest" + }, + "pomProperties": { + "$ref": "#/$defs/PomProperties" + }, + "pomProject": { + "$ref": "#/$defs/PomProject" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "virtualPath" + ] + }, + "KbPackageMetadata": { + "properties": { + "product_id": { + "type": "string" + }, + "kb": { + "type": "string" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ] + }, + "License": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ] + }, + "LinuxKernelMetadata": { + "properties": { + "name": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extendedVersion": { + "type": "string" + }, + "buildTime": { + "type": "string" + }, + "author": { + "type": "string" + }, + "format": { + "type": "string" + }, + "rwRootFS": { + "type": "boolean" + }, + "swapDevice": { + "type": "integer" + }, + "rootDevice": { + "type": "integer" + }, + "videoMode": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ] + }, + "LinuxKernelModuleMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "license": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "versionMagic": { + "type": "string" + }, + "parameters": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "$ref": "#/$defs/IDLikes" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "versionCodename": { + "type": "string" + }, + "buildID": { + "type": "string" + }, + "imageID": { + "type": "string" + }, + "imageVersion": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + }, + "supportEnd": { + "type": "string" + } + }, + "type": "object" + }, + "Location": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + }, + "annotations": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "MixLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "NixStoreMetadata": { + "properties": { + "outputHash": { + "type": "string" + }, + "output": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "outputHash", + "files" + ] + }, + "NpmPackageJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "private": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ] + }, + "NpmPackageLockJSONMetadata": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmMetadata" + }, + { + "$ref": "#/$defs/ApkMetadata" + }, + { + "$ref": "#/$defs/BinaryMetadata" + }, + { + "$ref": "#/$defs/CargoPackageMetadata" + }, + { + "$ref": "#/$defs/CocoapodsMetadata" + }, + { + "$ref": "#/$defs/ConanLockMetadata" + }, + { + "$ref": "#/$defs/ConanMetadata" + }, + { + "$ref": "#/$defs/DartPubMetadata" + }, + { + "$ref": "#/$defs/DotnetDepsMetadata" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableMetadata" + }, + { + "$ref": "#/$defs/DpkgMetadata" + }, + { + "$ref": "#/$defs/GemMetadata" + }, + { + "$ref": "#/$defs/GolangBinMetadata" + }, + { + "$ref": "#/$defs/GolangModMetadata" + }, + { + "$ref": "#/$defs/HackageMetadata" + }, + { + "$ref": "#/$defs/JavaMetadata" + }, + { + "$ref": "#/$defs/KbPackageMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelModuleMetadata" + }, + { + "$ref": "#/$defs/MixLockMetadata" + }, + { + "$ref": "#/$defs/NixStoreMetadata" + }, + { + "$ref": "#/$defs/NpmPackageJSONMetadata" + }, + { + "$ref": "#/$defs/NpmPackageLockJSONMetadata" + }, + { + "$ref": "#/$defs/PhpComposerJSONMetadata" + }, + { + "$ref": "#/$defs/PortageMetadata" + }, + { + "$ref": "#/$defs/PythonPackageMetadata" + }, + { + "$ref": "#/$defs/PythonPipfileLockMetadata" + }, + { + "$ref": "#/$defs/PythonRequirementsMetadata" + }, + { + "$ref": "#/$defs/RDescriptionFileMetadata" + }, + { + "$ref": "#/$defs/RebarLockMetadata" + }, + { + "$ref": "#/$defs/RpmMetadata" + }, + { + "$ref": "#/$defs/SwiftPackageManagerMetadata" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ] + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name" + ] + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ] + }, + "PhpComposerJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ] + }, + "PomParent": { + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ] + }, + "PomProject": { + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$ref": "#/$defs/PomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ] + }, + "PomProperties": { + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ] + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PortageMetadata": { + "properties": { + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ] + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "type": "object", + "required": [ + "url" + ] + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PythonPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ] + }, + "PythonPipfileLockMetadata": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "index": { + "type": "string" + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ] + }, + "PythonRequirementsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array" + }, + "versionConstraint": { + "type": "string" + }, + "url": { + "type": "string" + }, + "markers": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ] + }, + "RDescriptionFileMetadata": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "repository": { + "type": "string" + }, + "built": { + "type": "string" + }, + "needsCompilation": { + "type": "boolean" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "RebarLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "Relationship": { + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ] + }, + "RpmMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmdbFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "modularityLabel", + "files" + ] + }, + "RpmdbFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ] + }, + "Schema": { + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "version", + "url" + ] + }, + "SearchResult": { + "properties": { + "classification": { + "type": "string" + }, + "lineNumber": { + "type": "integer" + }, + "lineOffset": { + "type": "integer" + }, + "seekPosition": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "classification", + "lineNumber", + "lineOffset", + "seekPosition", + "length" + ] + }, + "Secrets": { + "properties": { + "location": { + "$ref": "#/$defs/Coordinates" + }, + "secrets": { + "items": { + "$ref": "#/$defs/SearchResult" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "location", + "secrets" + ] + }, + "Source": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ] + }, + "SwiftPackageManagerMetadata": { + "properties": { + "revision": { + "type": "string" + } + }, + "type": "object", + "required": [ + "revision" + ] + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-11.0.1.json b/schema/json/schema-11.0.1.json new file mode 100644 index 000000000..cd3e2b8da --- /dev/null +++ b/schema/json/schema-11.0.1.json @@ -0,0 +1,2003 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/11.0.1/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "gid": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "size": { + "type": "string" + }, + "link": { + "type": "string" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object" + }, + "AlpmMetadata": { + "properties": { + "basepackage": { + "type": "string" + }, + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "packager": { + "type": "string" + }, + "url": { + "type": "string" + }, + "validation": { + "type": "string" + }, + "reason": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ] + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "ApkMetadata": { + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "installedSize": { + "type": "integer" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ] + }, + "BinaryMetadata": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ] + }, + "CargoPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ] + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ] + }, + "CocoapodsMetadata": { + "properties": { + "checksum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "checksum" + ] + }, + "ConanLockMetadata": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "options": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "path": { + "type": "string" + }, + "context": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "ConanMetadata": { + "properties": { + "ref": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "Coordinates": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "DartPubMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hosted_url": { + "type": "string" + }, + "vcs_url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Descriptor": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": true + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array" + }, + "secrets": { + "items": { + "$ref": "#/$defs/Secrets" + }, + "type": "array" + }, + "source": { + "$ref": "#/$defs/Source" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor" + }, + "schema": { + "$ref": "#/$defs/Schema" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ] + }, + "DotnetDepsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sha512": { + "type": "string" + }, + "hashPath": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ] + }, + "DotnetPortableExecutableMetadata": { + "properties": { + "assemblyVersion": { + "type": "string" + }, + "legalCopyright": { + "type": "string" + }, + "comments": { + "type": "string" + }, + "internalName": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "productVersion": { + "type": "string" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ] + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ] + }, + "DpkgMetadata": { + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ] + }, + "File": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Coordinates" + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "id", + "location" + ] + }, + "FileLicense": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ] + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "extent": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ] + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ] + }, + "GemMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "GolangBinMetadata": { + "properties": { + "goBuildSettings": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + }, + "mainModule": { + "type": "string" + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ] + }, + "GolangModMetadata": { + "properties": { + "h1Digest": { + "type": "string" + } + }, + "type": "object" + }, + "HackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "snapshotURL": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "JavaManifest": { + "properties": { + "main": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "namedSections": { + "patternProperties": { + ".*": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "JavaMetadata": { + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest" + }, + "pomProperties": { + "$ref": "#/$defs/PomProperties" + }, + "pomProject": { + "$ref": "#/$defs/PomProject" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "virtualPath" + ] + }, + "KbPackageMetadata": { + "properties": { + "product_id": { + "type": "string" + }, + "kb": { + "type": "string" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ] + }, + "License": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ] + }, + "LinuxKernelMetadata": { + "properties": { + "name": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extendedVersion": { + "type": "string" + }, + "buildTime": { + "type": "string" + }, + "author": { + "type": "string" + }, + "format": { + "type": "string" + }, + "rwRootFS": { + "type": "boolean" + }, + "swapDevice": { + "type": "integer" + }, + "rootDevice": { + "type": "integer" + }, + "videoMode": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ] + }, + "LinuxKernelModuleMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "license": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "versionMagic": { + "type": "string" + }, + "parameters": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "$ref": "#/$defs/IDLikes" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "versionCodename": { + "type": "string" + }, + "buildID": { + "type": "string" + }, + "imageID": { + "type": "string" + }, + "imageVersion": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + }, + "supportEnd": { + "type": "string" + } + }, + "type": "object" + }, + "Location": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + }, + "annotations": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "MixLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "NixStoreMetadata": { + "properties": { + "outputHash": { + "type": "string" + }, + "output": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "outputHash", + "files" + ] + }, + "NpmPackageJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "private": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ] + }, + "NpmPackageLockJSONMetadata": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmMetadata" + }, + { + "$ref": "#/$defs/ApkMetadata" + }, + { + "$ref": "#/$defs/BinaryMetadata" + }, + { + "$ref": "#/$defs/CargoPackageMetadata" + }, + { + "$ref": "#/$defs/CocoapodsMetadata" + }, + { + "$ref": "#/$defs/ConanLockMetadata" + }, + { + "$ref": "#/$defs/ConanMetadata" + }, + { + "$ref": "#/$defs/DartPubMetadata" + }, + { + "$ref": "#/$defs/DotnetDepsMetadata" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableMetadata" + }, + { + "$ref": "#/$defs/DpkgMetadata" + }, + { + "$ref": "#/$defs/GemMetadata" + }, + { + "$ref": "#/$defs/GolangBinMetadata" + }, + { + "$ref": "#/$defs/GolangModMetadata" + }, + { + "$ref": "#/$defs/HackageMetadata" + }, + { + "$ref": "#/$defs/JavaMetadata" + }, + { + "$ref": "#/$defs/KbPackageMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelMetadata" + }, + { + "$ref": "#/$defs/LinuxKernelModuleMetadata" + }, + { + "$ref": "#/$defs/MixLockMetadata" + }, + { + "$ref": "#/$defs/NixStoreMetadata" + }, + { + "$ref": "#/$defs/NpmPackageJSONMetadata" + }, + { + "$ref": "#/$defs/NpmPackageLockJSONMetadata" + }, + { + "$ref": "#/$defs/PhpComposerJSONMetadata" + }, + { + "$ref": "#/$defs/PortageMetadata" + }, + { + "$ref": "#/$defs/PythonPackageMetadata" + }, + { + "$ref": "#/$defs/PythonPipfileLockMetadata" + }, + { + "$ref": "#/$defs/PythonRequirementsMetadata" + }, + { + "$ref": "#/$defs/RDescriptionFileMetadata" + }, + { + "$ref": "#/$defs/RebarLockMetadata" + }, + { + "$ref": "#/$defs/RpmMetadata" + }, + { + "$ref": "#/$defs/SwiftPackageManagerMetadata" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ] + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name" + ] + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ] + }, + "PhpComposerJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ] + }, + "PomParent": { + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ] + }, + "PomProject": { + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$ref": "#/$defs/PomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ] + }, + "PomProperties": { + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ] + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PortageMetadata": { + "properties": { + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ] + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "type": "object", + "required": [ + "url" + ] + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PythonPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ] + }, + "PythonPipfileLockMetadata": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "index": { + "type": "string" + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ] + }, + "PythonRequirementsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array" + }, + "versionConstraint": { + "type": "string" + }, + "url": { + "type": "string" + }, + "markers": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ] + }, + "RDescriptionFileMetadata": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "repository": { + "type": "string" + }, + "built": { + "type": "string" + }, + "needsCompilation": { + "type": "boolean" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "RebarLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "Relationship": { + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ] + }, + "RpmMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmdbFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "modularityLabel", + "files" + ] + }, + "RpmdbFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ] + }, + "Schema": { + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "version", + "url" + ] + }, + "SearchResult": { + "properties": { + "classification": { + "type": "string" + }, + "lineNumber": { + "type": "integer" + }, + "lineOffset": { + "type": "integer" + }, + "seekPosition": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "classification", + "lineNumber", + "lineOffset", + "seekPosition", + "length" + ] + }, + "Secrets": { + "properties": { + "location": { + "$ref": "#/$defs/Coordinates" + }, + "secrets": { + "items": { + "$ref": "#/$defs/SearchResult" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "location", + "secrets" + ] + }, + "Source": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ] + }, + "SwiftPackageManagerMetadata": { + "properties": { + "revision": { + "type": "string" + } + }, + "type": "object", + "required": [ + "revision" + ] + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/syft/event/event.go b/syft/event/event.go index 4c2a29296..c095e9eef 100644 --- a/syft/event/event.go +++ b/syft/event/event.go @@ -6,12 +6,10 @@ package event import ( "github.com/wagoodman/go-partybus" - - "github.com/anchore/syft/internal" ) const ( - typePrefix = internal.ApplicationName + typePrefix = "syft" cliTypePrefix = typePrefix + "-cli" // Events from the syft library @@ -48,7 +46,4 @@ const ( // CLINotification is a partybus event that occurs when auxiliary information is ready for presentation to stderr CLINotification partybus.EventType = cliTypePrefix + "-notification" - - // CLIExit is a partybus event that occurs when an analysis result is ready for final presentation - CLIExit partybus.EventType = cliTypePrefix + "-exit-event" ) diff --git a/syft/event/parsers/parsers.go b/syft/event/parsers/parsers.go index 2e6cbeca9..c2a56e17c 100644 --- a/syft/event/parsers/parsers.go +++ b/syft/event/parsers/parsers.go @@ -144,30 +144,22 @@ func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progres // CLI event types -func ParseCLIExit(e partybus.Event) (func() error, error) { - if err := checkEventType(e.Type, event.CLIExit); err != nil { +type UpdateCheck struct { + New string + Current string +} + +func ParseCLIAppUpdateAvailable(e partybus.Event) (*UpdateCheck, error) { + if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil { return nil, err } - fn, ok := e.Value.(func() error) + updateCheck, ok := e.Value.(UpdateCheck) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } - return fn, nil -} - -func ParseCLIAppUpdateAvailable(e partybus.Event) (string, error) { - if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil { - return "", err - } - - newVersion, ok := e.Value.(string) - if !ok { - return "", newPayloadErr(e.Type, "Value", e.Value) - } - - return newVersion, nil + return &updateCheck, nil } func ParseCLIReport(e partybus.Event) (string, string, error) { diff --git a/syft/formats/common/cyclonedxhelpers/decoder.go b/syft/formats/common/cyclonedxhelpers/decoder.go index ecfb9baf9..3400cf9ef 100644 --- a/syft/formats/common/cyclonedxhelpers/decoder.go +++ b/syft/formats/common/cyclonedxhelpers/decoder.go @@ -27,7 +27,8 @@ func GetValidator(format cyclonedx.BOMFileFormat) sbom.Validator { } xmlWithoutNS := format == cyclonedx.BOMFileFormatXML && !strings.Contains(bom.XMLNS, cycloneDXXmlSchema) - if (cyclonedx.BOM{} == *bom || bom.Components == nil || xmlWithoutNS) { + xmlWithoutComponents := format == cyclonedx.BOMFileFormatXML && bom.Components == nil + if (cyclonedx.BOM{} == *bom || xmlWithoutComponents || xmlWithoutNS) { return fmt.Errorf("not a valid CycloneDX document") } return nil @@ -210,24 +211,33 @@ func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]int return } for _, d := range *bom.Dependencies { - to, fromExists := idMap[d.Ref].(artifact.Identifiable) - if !fromExists { - continue - } - if d.Dependencies == nil { continue } + toPtr, toExists := idMap[d.Ref] + if !toExists { + continue + } + to, ok := common.PtrToStruct(toPtr).(artifact.Identifiable) + if !ok { + continue + } + for _, t := range *d.Dependencies { - from, toExists := idMap[t].(artifact.Identifiable) - if !toExists { + fromPtr, fromExists := idMap[t] + if !fromExists { + continue + } + from, ok := common.PtrToStruct(fromPtr).(artifact.Identifiable) + if !ok { continue } s.Relationships = append(s.Relationships, artifact.Relationship{ From: from, To: to, - Type: artifact.DependencyOfRelationship, // FIXME this information is lost + // match assumptions in encoding, that this is the only type of relationship captured: + Type: artifact.DependencyOfRelationship, }) } } diff --git a/syft/formats/common/cyclonedxhelpers/decoder_test.go b/syft/formats/common/cyclonedxhelpers/decoder_test.go index 7559dc02d..5d335d700 100644 --- a/syft/formats/common/cyclonedxhelpers/decoder_test.go +++ b/syft/formats/common/cyclonedxhelpers/decoder_test.go @@ -8,8 +8,10 @@ import ( "github.com/CycloneDX/cyclonedx-go" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" ) @@ -336,3 +338,101 @@ func Test_missingComponentsDecode(t *testing.T) { assert.NoError(t, err) } + +func Test_decodeDependencies(t *testing.T) { + c1 := cyclonedx.Component{ + Name: "c1", + } + + c2 := cyclonedx.Component{ + Name: "c2", + } + + c3 := cyclonedx.Component{ + Name: "c3", + } + + for _, c := range []*cyclonedx.Component{&c1, &c2, &c3} { + c.BOMRef = c.Name + } + + setTypes := func(typ cyclonedx.ComponentType, components ...cyclonedx.Component) *[]cyclonedx.Component { + var out []cyclonedx.Component + for _, c := range components { + c.Type = typ + out = append(out, c) + } + return &out + } + + tests := []struct { + name string + sbom cyclonedx.BOM + expected []string + }{ + { + name: "dependencies decoded as dependencyOf relationships", + sbom: cyclonedx.BOM{ + Components: setTypes(cyclonedx.ComponentTypeLibrary, + c1, + c2, + c3, + ), + Dependencies: &[]cyclonedx.Dependency{ + { + Ref: c1.BOMRef, + Dependencies: &[]string{ + c2.BOMRef, + c3.BOMRef, + }, + }, + }, + }, + expected: []string{c2.Name, c3.Name}, + }, + { + name: "dependencies skipped with unhandled components", + sbom: cyclonedx.BOM{ + Components: setTypes("", + c1, + c2, + c3, + ), + Dependencies: &[]cyclonedx.Dependency{ + { + Ref: c1.BOMRef, + Dependencies: &[]string{ + c2.BOMRef, + c3.BOMRef, + }, + }, + }, + }, + expected: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s, err := ToSyftModel(&test.sbom) + require.NoError(t, err) + require.NotNil(t, s) + + var deps []string + if s != nil { + for _, r := range s.Relationships { + if r.Type != artifact.DependencyOfRelationship { + continue + } + if p, ok := r.To.(pkg.Package); !ok || p.Name != c1.Name { + continue + } + if p, ok := r.From.(pkg.Package); ok { + deps = append(deps, p.Name) + } + } + } + require.Equal(t, test.expected, deps) + }) + } +} diff --git a/syft/formats/common/cyclonedxhelpers/external_references.go b/syft/formats/common/cyclonedxhelpers/external_references.go index 59f388717..4ba99587c 100644 --- a/syft/formats/common/cyclonedxhelpers/external_references.go +++ b/syft/formats/common/cyclonedxhelpers/external_references.go @@ -2,6 +2,7 @@ package cyclonedxhelpers import ( "fmt" + "net/url" "strings" "github.com/CycloneDX/cyclonedx-go" @@ -15,9 +16,11 @@ import ( func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { var refs []cyclonedx.ExternalReference if hasMetadata(p) { + // Skip adding extracted URL and Homepage metadata + // as "external_reference" if the metadata isn't IRI-compliant switch metadata := p.Metadata.(type) { case pkg.ApkMetadata: - if metadata.URL != "" { + if metadata.URL != "" && isValidExternalRef(metadata.URL) { refs = append(refs, cyclonedx.ExternalReference{ URL: metadata.URL, Type: cyclonedx.ERTypeDistribution, @@ -31,20 +34,20 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { }) } case pkg.NpmPackageJSONMetadata: - if metadata.URL != "" { + if metadata.URL != "" && isValidExternalRef(metadata.URL) { refs = append(refs, cyclonedx.ExternalReference{ URL: metadata.URL, Type: cyclonedx.ERTypeDistribution, }) } - if metadata.Homepage != "" { + if metadata.Homepage != "" && isValidExternalRef(metadata.Homepage) { refs = append(refs, cyclonedx.ExternalReference{ URL: metadata.Homepage, Type: cyclonedx.ERTypeWebsite, }) } case pkg.GemMetadata: - if metadata.Homepage != "" { + if metadata.Homepage != "" && isValidExternalRef(metadata.Homepage) { refs = append(refs, cyclonedx.ExternalReference{ URL: metadata.Homepage, Type: cyclonedx.ERTypeWebsite, @@ -94,7 +97,7 @@ func toCycloneDXAlgorithm(algorithm string) cyclonedx.HashAlgorithm { "sha256": cyclonedx.HashAlgorithm("SHA-256"), } - return validMap[algorithm] + return validMap[strings.ToLower(algorithm)] } func decodeExternalReferences(c *cyclonedx.Component, metadata interface{}) { @@ -158,3 +161,9 @@ func refComment(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) str } return "" } + +// isValidExternalRef checks for IRI-comppliance for input string to be added into "external_reference" +func isValidExternalRef(s string) bool { + parsed, err := url.Parse(s) + return err == nil && parsed != nil && parsed.Host != "" +} diff --git a/syft/formats/common/cyclonedxhelpers/external_references_test.go b/syft/formats/common/cyclonedxhelpers/external_references_test.go index c6ce0355b..72b983122 100644 --- a/syft/formats/common/cyclonedxhelpers/external_references_test.go +++ b/syft/formats/common/cyclonedxhelpers/external_references_test.go @@ -32,7 +32,7 @@ func Test_encodeExternalReferences(t *testing.T) { }, }, { - name: "from npm", + name: "from npm with valid URL", input: pkg.Package{ Metadata: pkg.NpmPackageJSONMetadata{ URL: "http://a-place.gov", @@ -42,6 +42,18 @@ func Test_encodeExternalReferences(t *testing.T) { {URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution}, }, }, + { + name: "from npm with invalid URL but valid Homepage", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + URL: "b-place", + Homepage: "http://b-place.gov", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://b-place.gov", Type: cyclonedx.ERTypeWebsite}, + }, + }, { name: "from cargo lock", input: pkg.Package{ @@ -132,3 +144,56 @@ func Test_encodeExternalReferences(t *testing.T) { }) } } + +func Test_isValidExternalRef(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "valid URL for external_reference, git protocol", + input: "git+https://github.com/abc/def.git", + expected: true, + }, + { + name: "valid URL for external_reference, git protocol", + input: "git+https://github.com/abc/def.git", + expected: true, + }, + { + name: "invalid URL for external_reference", + input: "abc/def", + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, isValidExternalRef(test.input)) + }) + } +} + +func Test_toCycloneDXAlgorithm(t *testing.T) { + tests := []struct { + name string + input string + expected cyclonedx.HashAlgorithm + }{ + { + name: "valid algorithm name in upper case", + input: "SHA1", + expected: cyclonedx.HashAlgorithm("SHA-1"), + }, + { + name: "valid algorithm name in lower case", + input: "sha1", + expected: cyclonedx.HashAlgorithm("SHA-1"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, toCycloneDXAlgorithm(test.input)) + }) + } +} diff --git a/syft/formats/common/cyclonedxhelpers/format.go b/syft/formats/common/cyclonedxhelpers/format.go index 34ca35094..f99e826d3 100644 --- a/syft/formats/common/cyclonedxhelpers/format.go +++ b/syft/formats/common/cyclonedxhelpers/format.go @@ -1,12 +1,13 @@ package cyclonedxhelpers import ( + "slices" + "strings" "time" "github.com/CycloneDX/cyclonedx-go" "github.com/google/uuid" - "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" @@ -23,7 +24,7 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { // https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36 // "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" cdxBOM.SerialNumber = uuid.New().URN() - cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, s.Descriptor.Version, s.Source) + cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source) packages := s.Artifacts.Packages.Sorted() components := make([]cyclonedx.Component, len(packages)) @@ -139,34 +140,53 @@ func isExpressiblePackageRelationship(ty artifact.RelationshipType) bool { } func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependency { - result := make([]cyclonedx.Dependency, 0) + dependencies := map[string]*cyclonedx.Dependency{} for _, r := range relationships { exists := isExpressiblePackageRelationship(r.Type) if !exists { - log.Debugf("unable to convert relationship from CycloneDX 1.4 JSON, dropping: %+v", r) + log.Debugf("unable to convert relationship type to CycloneDX JSON, dropping: %#v", r) continue } // we only capture package-to-package relationships for now - fromPkg, ok := r.From.(*pkg.Package) + fromPkg, ok := r.From.(pkg.Package) if !ok { + log.Tracef("unable to convert relationship fromPkg to CycloneDX JSON, dropping: %#v", r) continue } - toPkg, ok := r.To.(*pkg.Package) + toPkg, ok := r.To.(pkg.Package) if !ok { + log.Tracef("unable to convert relationship toPkg to CycloneDX JSON, dropping: %#v", r) continue } - // ind dep + toRef := deriveBomRef(toPkg) + dep := dependencies[toRef] + if dep == nil { + dep = &cyclonedx.Dependency{ + Ref: toRef, + Dependencies: &[]string{}, + } + dependencies[toRef] = dep + } - innerDeps := []string{} - innerDeps = append(innerDeps, deriveBomRef(*fromPkg)) - result = append(result, cyclonedx.Dependency{ - Ref: deriveBomRef(*toPkg), - Dependencies: &innerDeps, - }) + fromRef := deriveBomRef(fromPkg) + if !slices.Contains(*dep.Dependencies, fromRef) { + *dep.Dependencies = append(*dep.Dependencies, fromRef) + } } + + result := make([]cyclonedx.Dependency, 0, len(dependencies)) + for _, dep := range dependencies { + slices.Sort(*dep.Dependencies) + result = append(result, *dep) + } + + slices.SortFunc(result, func(a, b cyclonedx.Dependency) int { + return strings.Compare(a.Ref, b.Ref) + }) + return result } diff --git a/syft/formats/common/cyclonedxhelpers/format_test.go b/syft/formats/common/cyclonedxhelpers/format_test.go index 290e7152a..c62afb8f4 100644 --- a/syft/formats/common/cyclonedxhelpers/format_test.go +++ b/syft/formats/common/cyclonedxhelpers/format_test.go @@ -1,9 +1,16 @@ package cyclonedxhelpers import ( + "fmt" "testing" + "github.com/CycloneDX/cyclonedx-go" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" ) func Test_formatCPE(t *testing.T) { @@ -32,3 +39,102 @@ func Test_formatCPE(t *testing.T) { }) } } + +func Test_relationships(t *testing.T) { + p1 := pkg.Package{ + Name: "p1", + } + + p2 := pkg.Package{ + Name: "p2", + } + + p3 := pkg.Package{ + Name: "p3", + } + + p4 := pkg.Package{ + Name: "p4", + } + + for _, p := range []*pkg.Package{&p1, &p2, &p3, &p4} { + p.PURL = fmt.Sprintf("pkg:generic/%s@%s", p.Name, p.Name) + p.SetID() + } + + tests := []struct { + name string + sbom sbom.SBOM + expected *[]cyclonedx.Dependency + }{ + { + name: "package dependencyOf relationships output as dependencies", + sbom: sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: pkg.NewCollection(p1, p2, p3, p4), + }, + Relationships: []artifact.Relationship{ + { + From: p2, + To: p1, + Type: artifact.DependencyOfRelationship, + }, + { + From: p3, + To: p1, + Type: artifact.DependencyOfRelationship, + }, + { + From: p4, + To: p2, + Type: artifact.DependencyOfRelationship, + }, + }, + }, + expected: &[]cyclonedx.Dependency{ + { + Ref: deriveBomRef(p1), + Dependencies: &[]string{ + deriveBomRef(p2), + deriveBomRef(p3), + }, + }, + { + Ref: deriveBomRef(p2), + Dependencies: &[]string{ + deriveBomRef(p4), + }, + }, + }, + }, + { + name: "package contains relationships not output", + sbom: sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: pkg.NewCollection(p1, p2, p3), + }, + Relationships: []artifact.Relationship{ + { + From: p2, + To: p1, + Type: artifact.ContainsRelationship, + }, + { + From: p3, + To: p1, + Type: artifact.ContainsRelationship, + }, + }, + }, + expected: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cdx := ToFormatModel(test.sbom) + got := cdx.Dependencies + require.Equal(t, test.expected, got) + }) + } +} diff --git a/syft/formats/common/spdxhelpers/document_namespace.go b/syft/formats/common/spdxhelpers/document_namespace.go index 3b6d30b69..74801653d 100644 --- a/syft/formats/common/spdxhelpers/document_namespace.go +++ b/syft/formats/common/spdxhelpers/document_namespace.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" - "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -18,12 +18,12 @@ const ( inputFile = "file" ) -func DocumentNameAndNamespace(src source.Description) (string, string) { +func DocumentNameAndNamespace(src source.Description, desc sbom.Descriptor) (string, string) { name := DocumentName(src) - return name, DocumentNamespace(name, src) + return name, DocumentNamespace(name, src, desc) } -func DocumentNamespace(name string, src source.Description) string { +func DocumentNamespace(name string, src source.Description, desc sbom.Descriptor) string { name = cleanName(name) input := "unknown-source-type" switch src.Metadata.(type) { @@ -44,7 +44,7 @@ func DocumentNamespace(name string, src source.Description) string { u := url.URL{ Scheme: "https", Host: "anchore.com", - Path: path.Join(internal.ApplicationName, identifier), + Path: path.Join(desc.Name, identifier), } return u.String() diff --git a/syft/formats/common/spdxhelpers/document_namespace_test.go b/syft/formats/common/spdxhelpers/document_namespace_test.go index 00bed3536..9fa2ccb33 100644 --- a/syft/formats/common/spdxhelpers/document_namespace_test.go +++ b/syft/formats/common/spdxhelpers/document_namespace_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/anchore/syft/syft/internal/sourcemetadata" + "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -55,9 +56,12 @@ func Test_documentNamespace(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual := DocumentNamespace(test.inputName, test.src) + actual := DocumentNamespace(test.inputName, test.src, sbom.Descriptor{ + Name: "syft", + }) + // note: since the namespace ends with a UUID we check the prefix - assert.True(t, strings.HasPrefix(actual, test.expected), fmt.Sprintf("actual namespace %q", actual)) + assert.True(t, strings.HasPrefix(actual, test.expected), fmt.Sprintf("expected prefix: '%s' got: '%s'", test.expected, actual)) // track each scheme tested (passed or not) tracker.Tested(t, test.src.Metadata) diff --git a/syft/formats/common/spdxhelpers/source_info.go b/syft/formats/common/spdxhelpers/source_info.go index 2ec80786e..c4942aeca 100644 --- a/syft/formats/common/spdxhelpers/source_info.go +++ b/syft/formats/common/spdxhelpers/source_info.go @@ -56,6 +56,8 @@ func SourceInfo(p pkg.Package) string { answer = "acquired package info from R-package DESCRIPTION file" case pkg.SwiftPkg: answer = "acquired package info from resolved Swift package manifest" + case pkg.GithubActionPkg, pkg.GithubActionWorkflowPkg: + answer = "acquired package info from GitHub Actions workflow file or composite action file" default: answer = "acquired package info from the following paths" } diff --git a/syft/formats/common/spdxhelpers/source_info_test.go b/syft/formats/common/spdxhelpers/source_info_test.go index 5b30b95fc..4fd221a9f 100644 --- a/syft/formats/common/spdxhelpers/source_info_test.go +++ b/syft/formats/common/spdxhelpers/source_info_test.go @@ -239,6 +239,22 @@ func Test_SourceInfo(t *testing.T) { "from resolved Swift package manifest", }, }, + { + input: pkg.Package{ + Type: pkg.GithubActionPkg, + }, + expected: []string{ + "from GitHub Actions workflow file or composite action file", + }, + }, + { + input: pkg.Package{ + Type: pkg.GithubActionWorkflowPkg, + }, + expected: []string{ + "from GitHub Actions workflow file or composite action file", + }, + }, } var pkgTypes []pkg.Type for _, test := range tests { diff --git a/syft/formats/common/spdxhelpers/to_format_model.go b/syft/formats/common/spdxhelpers/to_format_model.go index 258c96f8f..62f809644 100644 --- a/syft/formats/common/spdxhelpers/to_format_model.go +++ b/syft/formats/common/spdxhelpers/to_format_model.go @@ -5,14 +5,13 @@ import ( "crypto/sha1" "fmt" "path" + "slices" "sort" "strings" "time" - "github.com/docker/distribution/reference" + "github.com/distribution/reference" "github.com/spdx/tools-golang/spdx" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" "github.com/anchore/packageurl-go" "github.com/anchore/syft/internal" @@ -44,7 +43,7 @@ const ( // //nolint:funlen func ToFormatModel(s sbom.SBOM) *spdx.Document { - name, namespace := DocumentNameAndNamespace(s.Source) + name, namespace := DocumentNameAndNamespace(s.Source, s.Descriptor) packages := toPackages(s.Artifacts.Packages, s) @@ -136,7 +135,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document { CreatorType: "Organization", }, { - Creator: internal.ApplicationName + "-" + s.Descriptor.Version, + Creator: s.Descriptor.Name + "-" + s.Descriptor.Version, CreatorType: "Tool", }, }, @@ -722,7 +721,11 @@ func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense { var result []*spdx.OtherLicense - ids := maps.Keys(licenses) + var ids []string + for licenseID := range licenses { + ids = append(ids, licenseID) + } + slices.Sort(ids) for _, id := range ids { license := licenses[id] diff --git a/syft/formats/common/spdxhelpers/to_syft_model.go b/syft/formats/common/spdxhelpers/to_syft_model.go index c17b6367e..f61f723c8 100644 --- a/syft/formats/common/spdxhelpers/to_syft_model.go +++ b/syft/formats/common/spdxhelpers/to_syft_model.go @@ -286,6 +286,16 @@ func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, packages []*spd } func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document) { + for _, p := range doc.Packages { + for _, f := range p.Files { + l := toSyftLocation(f) + spdxIDMap[string(f.FileSPDXIdentifier)] = l + + s.Artifacts.FileMetadata[l.Coordinates] = toFileMetadata(f) + s.Artifacts.FileDigests[l.Coordinates] = toFileDigests(f) + } + } + for _, f := range doc.Files { l := toSyftLocation(f) spdxIDMap[string(f.FileSPDXIdentifier)] = l @@ -332,7 +342,14 @@ func toFileMetadata(f *spdx.File) (meta file.Metadata) { } func toSyftRelationships(spdxIDMap map[string]any, doc *spdx.Document) []artifact.Relationship { - var out []artifact.Relationship + out := collectDocRelationships(spdxIDMap, doc) + + out = append(out, collectPackageFileRelationships(spdxIDMap, doc)...) + + return out +} + +func collectDocRelationships(spdxIDMap map[string]any, doc *spdx.Document) (out []artifact.Relationship) { for _, r := range doc.Relationships { // FIXME what to do with r.RefA.DocumentRefID and r.RefA.SpecialID if r.RefA.DocumentRefID != "" && requireAndTrimPrefix(r.RefA.DocumentRefID, "DocumentRef-") != string(doc.SPDXIdentifier) { @@ -386,6 +403,30 @@ func toSyftRelationships(spdxIDMap map[string]any, doc *spdx.Document) []artifac return out } +// collectPackageFileRelationships add relationships for direct files +func collectPackageFileRelationships(spdxIDMap map[string]any, doc *spdx.Document) (out []artifact.Relationship) { + for _, p := range doc.Packages { + a := spdxIDMap[string(p.PackageSPDXIdentifier)] + from, fromOk := a.(pkg.Package) + if !fromOk { + continue + } + for _, f := range p.Files { + b := spdxIDMap[string(f.FileSPDXIdentifier)] + to, toLocationOk := b.(file.Location) + if !toLocationOk { + continue + } + out = append(out, artifact.Relationship{ + From: from, + To: to, + Type: artifact.ContainsRelationship, + }) + } + } + return out +} + func toSyftCoordinates(f *spdx.File) file.Coordinates { const layerIDPrefix = "layerID: " var fileSystemID string diff --git a/syft/formats/common/spdxhelpers/to_syft_model_test.go b/syft/formats/common/spdxhelpers/to_syft_model_test.go index f6023f662..eb997eb35 100644 --- a/syft/formats/common/spdxhelpers/to_syft_model_test.go +++ b/syft/formats/common/spdxhelpers/to_syft_model_test.go @@ -608,3 +608,77 @@ func Test_purlValue(t *testing.T) { }) } } + +func Test_directPackageFiles(t *testing.T) { + doc := &spdx.Document{ + SPDXVersion: "SPDX-2.3", + Packages: []*spdx.Package{ + { + PackageName: "some-package", + PackageSPDXIdentifier: "1", + PackageVersion: "1.0.5", + Files: []*spdx.File{ + { + FileName: "some-file", + FileSPDXIdentifier: "2", + Checksums: []spdx.Checksum{ + { + Algorithm: "SHA1", + Value: "a8d733c64f9123", + }, + }, + }, + }, + }, + }, + } + + got, err := ToSyftModel(doc) + require.NoError(t, err) + + p := pkg.Package{ + Name: "some-package", + Version: "1.0.5", + MetadataType: pkg.UnknownMetadataType, + } + p.SetID() + f := file.Location{ + LocationData: file.LocationData{ + Coordinates: file.Coordinates{ + RealPath: "some-file", + FileSystemID: "", + }, + VirtualPath: "some-file", + }, + LocationMetadata: file.LocationMetadata{ + Annotations: map[string]string{}, + }, + } + s := &sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: pkg.NewCollection(p), + FileMetadata: map[file.Coordinates]file.Metadata{ + f.Coordinates: {}, + }, + FileDigests: map[file.Coordinates][]file.Digest{ + f.Coordinates: { + { + Algorithm: "sha1", + Value: "a8d733c64f9123", + }, + }, + }, + }, + Relationships: []artifact.Relationship{ + { + From: p, + To: f, + Type: artifact.ContainsRelationship, + }, + }, + Source: source.Description{}, + Descriptor: sbom.Descriptor{}, + } + + require.Equal(t, s, got) +} diff --git a/syft/formats/cyclonedxjson/encoder.go b/syft/formats/cyclonedxjson/encoder.go index 13ad32f68..297b80265 100644 --- a/syft/formats/cyclonedxjson/encoder.go +++ b/syft/formats/cyclonedxjson/encoder.go @@ -9,11 +9,40 @@ import ( "github.com/anchore/syft/syft/sbom" ) -func encoder(output io.Writer, s sbom.SBOM) error { +func encoderV1_0(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_0) +} + +func encoderV1_1(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_1) +} + +func encoderV1_2(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_2) +} + +func encoderV1_3(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_3) +} + +func encoderV1_4(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_4) +} + +func encoderV1_5(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_5) +} + +func buildEncoder(output io.Writer, s sbom.SBOM) (cyclonedx.BOMEncoder, *cyclonedx.BOM) { bom := cyclonedxhelpers.ToFormatModel(s) enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatJSON) enc.SetPretty(true) enc.SetEscapeHTML(false) - err := enc.Encode(bom) - return err + return enc, bom } diff --git a/syft/formats/cyclonedxjson/format.go b/syft/formats/cyclonedxjson/format.go index 8c1e2e016..97a088aae 100644 --- a/syft/formats/cyclonedxjson/format.go +++ b/syft/formats/cyclonedxjson/format.go @@ -9,10 +9,62 @@ import ( const ID sbom.FormatID = "cyclonedx-json" -func Format() sbom.Format { +var Format = Format1_4 + +func Format1_0() sbom.Format { return sbom.NewFormat( - sbom.AnyVersion, - encoder, + cyclonedx.SpecVersion1_0.String(), + encoderV1_0, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), + ID, + ) +} + +func Format1_1() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_1.String(), + encoderV1_1, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), + ID, + ) +} + +func Format1_2() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_2.String(), + encoderV1_2, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), + ID, + ) +} + +func Format1_3() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_3.String(), + encoderV1_3, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), + ID, + ) +} + +func Format1_4() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_4.String(), + encoderV1_4, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), + ID, + ) +} + +func Format1_5() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_5.String(), + encoderV1_5, cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), ID, diff --git a/syft/formats/cyclonedxjson/format_test.go b/syft/formats/cyclonedxjson/format_test.go new file mode 100644 index 000000000..2cfd5e545 --- /dev/null +++ b/syft/formats/cyclonedxjson/format_test.go @@ -0,0 +1,34 @@ +package cyclonedxjson + +import ( + "testing" + + "github.com/CycloneDX/cyclonedx-go" +) + +func TestFormatVersions(t *testing.T) { + tests := []struct { + name string + expectedVersion string + }{ + { + + "cyclonedx-json should default to v1.4", + cyclonedx.SpecVersion1_4.String(), + }, + } + + for _, c := range tests { + c := c + t.Run(c.name, func(t *testing.T) { + sbomFormat := Format() + if sbomFormat.ID() != ID { + t.Errorf("expected ID %q, got %q", ID, sbomFormat.ID()) + } + + if sbomFormat.Version() != c.expectedVersion { + t.Errorf("expected version %q, got %q", c.expectedVersion, sbomFormat.Version()) + } + }) + } +} diff --git a/syft/formats/cyclonedxxml/encoder.go b/syft/formats/cyclonedxxml/encoder.go index b8abdf81a..3941feca0 100644 --- a/syft/formats/cyclonedxxml/encoder.go +++ b/syft/formats/cyclonedxxml/encoder.go @@ -9,11 +9,40 @@ import ( "github.com/anchore/syft/syft/sbom" ) -func encoder(output io.Writer, s sbom.SBOM) error { +func encoderV1_0(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_0) +} + +func encoderV1_1(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_1) +} + +func encoderV1_2(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_2) +} + +func encoderV1_3(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_3) +} + +func encoderV1_4(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_4) +} + +func encoderV1_5(output io.Writer, s sbom.SBOM) error { + enc, bom := buildEncoder(output, s) + return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_5) +} + +func buildEncoder(output io.Writer, s sbom.SBOM) (cyclonedx.BOMEncoder, *cyclonedx.BOM) { bom := cyclonedxhelpers.ToFormatModel(s) enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatXML) enc.SetPretty(true) - - err := enc.Encode(bom) - return err + enc.SetEscapeHTML(false) + return enc, bom } diff --git a/syft/formats/cyclonedxxml/encoder_test.go b/syft/formats/cyclonedxxml/encoder_test.go index 5b8781aff..17e1eb597 100644 --- a/syft/formats/cyclonedxxml/encoder_test.go +++ b/syft/formats/cyclonedxxml/encoder_test.go @@ -57,7 +57,7 @@ func redactor(values ...string) testutils.Redactor { `sha256:[A-Za-z0-9]{64}`: `sha256:redacted`, // BOM refs - `bom-ref="[a-zA-Z0-9\-:]+"`: `bom-ref:redacted`, + `bom-ref="[a-zA-Z0-9\-:]+"`: `bom-ref="redacted"`, }, ) } diff --git a/syft/formats/cyclonedxxml/format.go b/syft/formats/cyclonedxxml/format.go index 7fe53c4f7..1b22cee14 100644 --- a/syft/formats/cyclonedxxml/format.go +++ b/syft/formats/cyclonedxxml/format.go @@ -9,10 +9,62 @@ import ( const ID sbom.FormatID = "cyclonedx-xml" -func Format() sbom.Format { +var Format = Format1_4 + +func Format1_0() sbom.Format { return sbom.NewFormat( - sbom.AnyVersion, - encoder, + cyclonedx.SpecVersion1_0.String(), + encoderV1_0, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), + ID, "cyclonedx", "cyclone", + ) +} + +func Format1_1() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_1.String(), + encoderV1_1, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), + ID, "cyclonedx", "cyclone", + ) +} + +func Format1_2() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_2.String(), + encoderV1_2, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), + ID, "cyclonedx", "cyclone", + ) +} + +func Format1_3() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_3.String(), + encoderV1_3, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), + ID, "cyclonedx", "cyclone", + ) +} + +func Format1_4() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_4.String(), + encoderV1_4, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), + ID, "cyclonedx", "cyclone", + ) +} + +func Format1_5() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_5.String(), + encoderV1_5, cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), ID, "cyclonedx", "cyclone", diff --git a/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index 592072d20..bbba18034 100644 --- a/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -32,7 +32,7 @@ /some/path/pkg1 - + package-2 2.0.1 cpe:2.3:*:some:package:2:*:*:*:*:*:*:* diff --git a/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 95701d100..914602f17 100644 --- a/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/syft/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -34,7 +34,7 @@ /somefile-1.txt - + package-2 2.0.1 cpe:2.3:*:some:package:2:*:*:*:*:*:*:* diff --git a/syft/formats/formats.go b/syft/formats/formats.go index 13af6cf05..e18b1a852 100644 --- a/syft/formats/formats.go +++ b/syft/formats/formats.go @@ -6,10 +6,9 @@ import ( "fmt" "io" "regexp" + "slices" "strings" - "golang.org/x/exp/slices" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/formats/cyclonedxjson" "github.com/anchore/syft/syft/formats/cyclonedxxml" @@ -26,17 +25,27 @@ import ( func Formats() []sbom.Format { return []sbom.Format{ syftjson.Format(), - cyclonedxxml.Format(), - cyclonedxjson.Format(), github.Format(), + table.Format(), + text.Format(), + template.Format(), + cyclonedxxml.Format1_0(), + cyclonedxxml.Format1_1(), + cyclonedxxml.Format1_2(), + cyclonedxxml.Format1_3(), + cyclonedxxml.Format1_4(), + cyclonedxxml.Format1_5(), + cyclonedxjson.Format1_0(), + cyclonedxjson.Format1_1(), + cyclonedxjson.Format1_2(), + cyclonedxjson.Format1_3(), + cyclonedxjson.Format1_4(), + cyclonedxjson.Format1_5(), spdxtagvalue.Format2_1(), spdxtagvalue.Format2_2(), spdxtagvalue.Format2_3(), spdxjson.Format2_2(), spdxjson.Format2_3(), - table.Format(), - text.Format(), - template.Format(), } } @@ -55,7 +64,7 @@ func Identify(by []byte) sbom.Format { // ByName accepts a name@version string, such as: // -// spdx-json@2.1 or cyclonedx@2 +// spdx-json@2.1 or cyclonedx@1.5 func ByName(name string) sbom.Format { parts := strings.SplitN(name, "@", 2) version := sbom.AnyVersion @@ -71,6 +80,16 @@ func ByNameAndVersion(name string, version string) sbom.Format { for _, f := range Formats() { for _, n := range f.IDs() { if cleanFormatName(string(n)) == name && versionMatches(f.Version(), version) { + // if the version is not specified and the format is cyclonedx, then we want to return the most recent version up to 1.4 + // If more aliases like cdx are added this will not catch those - we want to eventually provide a way for + // formats to inform this function what their default version is + // TODO: remove this check when 1.5 is stable or default formats are designed. PR below should be merged. + // https://github.com/CycloneDX/cyclonedx-go/pull/90 + if version == sbom.AnyVersion && strings.Contains(string(n), "cyclone") { + if f.Version() == "1.5" { + continue + } + } if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() { mostRecentFormat = f } diff --git a/syft/formats/formats_test.go b/syft/formats/formats_test.go index 60dae72c9..2cdc99b8a 100644 --- a/syft/formats/formats_test.go +++ b/syft/formats/formats_test.go @@ -70,7 +70,6 @@ func TestFormats_EmptyInput(t *testing.T) { } func TestByName(t *testing.T) { - tests := []struct { name string want sbom.FormatID diff --git a/syft/formats/github/encoder.go b/syft/formats/github/encoder.go index 261ff6b18..6f8ff5719 100644 --- a/syft/formats/github/encoder.go +++ b/syft/formats/github/encoder.go @@ -8,7 +8,6 @@ import ( "github.com/mholt/archiver/v3" "github.com/anchore/packageurl-go" - "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" @@ -26,7 +25,7 @@ func toGithubModel(s *sbom.SBOM) DependencySnapshot { Version: 0, // TODO allow property input to specify the Job, Sha, and Ref Detector: DetectorMetadata{ - Name: internal.ApplicationName, + Name: s.Descriptor.Name, URL: "https://github.com/anchore/syft", Version: v, }, diff --git a/syft/formats/github/encoder_test.go b/syft/formats/github/encoder_test.go index 3325509ce..e027c7936 100644 --- a/syft/formats/github/encoder_test.go +++ b/syft/formats/github/encoder_test.go @@ -18,6 +18,9 @@ import ( func sbomFixture() sbom.SBOM { s := sbom.SBOM{ + Descriptor: sbom.Descriptor{ + Name: "syft", + }, Source: source.Description{ Metadata: source.StereoscopeImageSourceMetadata{ UserInput: "ubuntu:18.04", diff --git a/syft/formats/internal/testutils/snapshot.go b/syft/formats/internal/testutils/snapshot.go index 7eae36594..f06850b1d 100644 --- a/syft/formats/internal/testutils/snapshot.go +++ b/syft/formats/internal/testutils/snapshot.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "github.com/sergi/go-diff/diffmatchpatch" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -64,12 +64,16 @@ func AssertEncoderAgainstGoldenSnapshot(t *testing.T, cfg EncoderSnapshotTestCon if cfg.IsJSON { require.JSONEq(t, string(expected), string(actual)) - } else if !bytes.Equal(expected, actual) { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(string(expected), string(actual), true) - t.Logf("len: %d\nexpected: %s", len(expected), expected) - t.Logf("len: %d\nactual: %s", len(actual), actual) - t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) + } else { + requireEqual(t, expected, actual) + } +} + +func requireEqual(t *testing.T, expected any, actual any) { + if diff := cmp.Diff(expected, actual); diff != "" { + t.Logf("expected: %s", expected) + t.Logf("actual: %s", actual) + t.Fatalf("mismatched output: %s", diff) } } diff --git a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden index dac57a1da..b5f1c29a7 100644 --- a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden +++ b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden @@ -39,7 +39,7 @@ }, { "name": "package-2", - "SPDXID": "SPDXRef-Package-deb-package-2-db4abfe497c180d3", + "SPDXID": "SPDXRef-Package-deb-package-2-ad5013466727018f", "versionInfo": "2.0.1", "supplier": "NOASSERTION", "downloadLocation": "NOASSERTION", @@ -78,7 +78,7 @@ }, { "spdxElementId": "SPDXRef-DocumentRoot-Directory-some-path", - "relatedSpdxElement": "SPDXRef-Package-deb-package-2-db4abfe497c180d3", + "relatedSpdxElement": "SPDXRef-Package-deb-package-2-ad5013466727018f", "relationshipType": "CONTAINS" }, { diff --git a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden index 73e27667e..009333c6d 100644 --- a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden +++ b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden @@ -39,7 +39,7 @@ }, { "name": "package-2", - "SPDXID": "SPDXRef-Package-deb-package-2-958443e2d9304af4", + "SPDXID": "SPDXRef-Package-deb-package-2-f27313b22a5ba330", "versionInfo": "2.0.1", "supplier": "NOASSERTION", "downloadLocation": "NOASSERTION", @@ -92,7 +92,7 @@ }, { "spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input", - "relatedSpdxElement": "SPDXRef-Package-deb-package-2-958443e2d9304af4", + "relatedSpdxElement": "SPDXRef-Package-deb-package-2-f27313b22a5ba330", "relationshipType": "CONTAINS" }, { diff --git a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden index 80e5b269a..260d4cba0 100644 --- a/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden +++ b/syft/formats/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden @@ -39,7 +39,7 @@ }, { "name": "package-2", - "SPDXID": "SPDXRef-Package-deb-package-2-958443e2d9304af4", + "SPDXID": "SPDXRef-Package-deb-package-2-f27313b22a5ba330", "versionInfo": "2.0.1", "supplier": "NOASSERTION", "downloadLocation": "NOASSERTION", @@ -214,7 +214,7 @@ }, { "spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input", - "relatedSpdxElement": "SPDXRef-Package-deb-package-2-958443e2d9304af4", + "relatedSpdxElement": "SPDXRef-Package-deb-package-2-f27313b22a5ba330", "relationshipType": "CONTAINS" }, { diff --git a/syft/formats/spdxtagvalue/decoder_test.go b/syft/formats/spdxtagvalue/decoder_test.go index 85f0b01c3..6802e1b5c 100644 --- a/syft/formats/spdxtagvalue/decoder_test.go +++ b/syft/formats/spdxtagvalue/decoder_test.go @@ -2,9 +2,13 @@ package spdxtagvalue import ( "os" + "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" ) // TODO: this is a temporary coverage see below @@ -34,3 +38,59 @@ func TestSPDXTagValueDecoder(t *testing.T) { }) } } + +func Test_packageDirectFiles(t *testing.T) { + contents := ` +SPDXVersion: SPDX-2.2 +DataLicense: CC0-1.0 +SPDXID: SPDXRef-DOCUMENT +DocumentName: Some-SBOM +DocumentNamespace: https://example.org/some/namespace +Creator: Organization: Some-organization +Creator: Tool: Some-tool Version: 1.0 +Created: 2021-12-29T17:02:21Z +PackageName: Some-package +PackageVersion: 5.1.2 +SPDXID: SPDXRef-Package-43c51b08-cc7e-406d-8ad9-34aa292d1157 +PackageSupplier: Organization: Some-organization +PackageDownloadLocation: https://example.org/download/location +FilesAnalyzed: true +PackageLicenseInfoFromFiles: NOASSERTION +PackageVerificationCode: 23460C5559C8D4DE3F6504E0E84E844CAC8B1D95 +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageCopyrightText: NOASSERTION +PackageChecksum: SHA1: 23460C5559C8D4DE3F6504E0E84E844CAC8B1D95 +FileName: Some-file-name +SPDXID: SPDXRef-99545d55-933d-4e08-9eb5-9d826111cb79 +FileContributor: Some-file-contributor +FileType: BINARY +FileChecksum: SHA1: 23460C5559C8D4DE3F6504E0E84E844CAC8B1D95 +LicenseConcluded: NOASSERTION +LicenseInfoInFile: NOASSERTION +FileCopyrightText: NOASSERTION +` + + s, err := decoder(strings.NewReader(contents)) + require.NoError(t, err) + + pkgs := s.Artifacts.Packages.Sorted() + assert.Len(t, pkgs, 1) + assert.Len(t, s.Artifacts.FileMetadata, 1) + assert.Len(t, s.Relationships, 1) + p := pkgs[0] + r := s.Relationships[0] + f := file.Location{} + for c := range s.Artifacts.FileMetadata { + f = file.Location{ + LocationData: file.LocationData{ + Coordinates: c, + VirtualPath: "", + }, + LocationMetadata: file.LocationMetadata{}, + } + break // there should only be 1 + } + assert.Equal(t, p.ID(), r.From.ID()) + assert.Equal(t, f.ID(), r.To.ID()) +} diff --git a/syft/formats/spdxtagvalue/encoder_test.go b/syft/formats/spdxtagvalue/encoder_test.go index baafe2b02..e5fe4a5af 100644 --- a/syft/formats/spdxtagvalue/encoder_test.go +++ b/syft/formats/spdxtagvalue/encoder_test.go @@ -115,7 +115,7 @@ func redactor(values ...string) testutils.Redactor { `Created: .*`: "Created: redacted", // each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing - `DocumentNamespace: https://anchore.com/syft/.*`: "DocumentNamespace: redacted", + `DocumentNamespace: https://anchore.com/.*`: "DocumentNamespace: redacted", // the license list will be updated periodically, the value here should not be directly tested in snapshot tests `LicenseListVersion: .*`: "LicenseListVersion: redacted", diff --git a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden index 764ac9830..8b42da861 100644 --- a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden +++ b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden @@ -61,7 +61,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256:2731251dc34951 ##### Package: package-2 PackageName: package-2 -SPDXID: SPDXRef-Package-deb-package-2-958443e2d9304af4 +SPDXID: SPDXRef-Package-deb-package-2-f27313b22a5ba330 PackageVersion: 2.0.1 PackageSupplier: NOASSERTION PackageDownloadLocation: NOASSERTION @@ -97,6 +97,6 @@ Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-125840abc1c66dd7 -Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-958443e2d9304af4 +Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-f27313b22a5ba330 Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input diff --git a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden index e50194535..7cfc999cb 100644 --- a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden +++ b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden @@ -20,7 +20,7 @@ FilesAnalyzed: false ##### Package: package-2 PackageName: package-2 -SPDXID: SPDXRef-Package-deb-package-2-db4abfe497c180d3 +SPDXID: SPDXRef-Package-deb-package-2-ad5013466727018f PackageVersion: 2.0.1 PackageSupplier: NOASSERTION PackageDownloadLocation: NOASSERTION @@ -50,6 +50,6 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-2 ##### Relationships Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-python-package-1-9265397e5e15168a -Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-deb-package-2-db4abfe497c180d3 +Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-deb-package-2-ad5013466727018f Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Directory-some-path diff --git a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden index 3c640ea72..8c3d1f705 100644 --- a/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden +++ b/syft/formats/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden @@ -23,7 +23,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256:2731251dc34951 ##### Package: package-2 PackageName: package-2 -SPDXID: SPDXRef-Package-deb-package-2-958443e2d9304af4 +SPDXID: SPDXRef-Package-deb-package-2-f27313b22a5ba330 PackageVersion: 2.0.1 PackageSupplier: NOASSERTION PackageDownloadLocation: NOASSERTION @@ -53,6 +53,6 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1 ##### Relationships Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-125840abc1c66dd7 -Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-958443e2d9304af4 +Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-f27313b22a5ba330 Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input diff --git a/syft/formats/syftjson/decoder_test.go b/syft/formats/syftjson/decoder_test.go index e0de5fffb..e0e184a3e 100644 --- a/syft/formats/syftjson/decoder_test.go +++ b/syft/formats/syftjson/decoder_test.go @@ -9,8 +9,17 @@ import ( "github.com/go-test/deep" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + stereoscopeFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/formats/internal/testutils" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" ) func TestEncodeDecodeCycle(t *testing.T) { @@ -86,3 +95,145 @@ func TestOutOfDateParser(t *testing.T) { }) } } + +func Test_encodeDecodeFileMetadata(t *testing.T) { + p := pkg.Package{ + Name: "pkg", + Version: "version", + FoundBy: "something", + Locations: file.NewLocationSet(file.Location{ + LocationData: file.LocationData{ + Coordinates: file.Coordinates{ + RealPath: "/somewhere", + FileSystemID: "id", + }, + }, + LocationMetadata: file.LocationMetadata{ + Annotations: map[string]string{ + "key": "value", + }, + }, + }), + Licenses: pkg.NewLicenseSet(pkg.License{ + Value: "MIT", + SPDXExpression: "MIT", + Type: "MIT", + URLs: internal.NewStringSet("https://example.org/license"), + Locations: file.LocationSet{}, + }), + Language: "language", + Type: "type", + CPEs: []cpe.CPE{ + { + Part: "a", + Vendor: "vendor", + Product: "product", + Version: "version", + Update: "update", + }, + }, + PURL: "pkg:generic/pkg@version", + MetadataType: "", + Metadata: nil, + } + p.SetID() + + c := file.Coordinates{ + RealPath: "some-file", + FileSystemID: "some-fs-id", + } + + s := sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: pkg.NewCollection(p), + FileMetadata: map[file.Coordinates]file.Metadata{ + c: { + FileInfo: stereoscopeFile.ManualInfo{ + NameValue: c.RealPath, + ModeValue: 0644, + SizeValue: 7, + }, + Path: c.RealPath, + Type: stereoscopeFile.TypeRegular, + UserID: 1, + GroupID: 2, + MIMEType: "text/plain", + }, + }, + FileDigests: map[file.Coordinates][]file.Digest{ + c: { + { + Algorithm: "sha1", + Value: "d34db33f", + }, + }, + }, + FileContents: map[file.Coordinates]string{ + c: "some contents", + }, + FileLicenses: map[file.Coordinates][]file.License{ + c: { + { + Value: "MIT", + SPDXExpression: "MIT", + Type: "MIT", + LicenseEvidence: &file.LicenseEvidence{ + Confidence: 1, + Offset: 2, + Extent: 3, + }, + }, + }, + }, + LinuxDistribution: &linux.Release{ + PrettyName: "some os", + Name: "os", + ID: "os-id", + IDLike: []string{"os"}, + Version: "version", + VersionID: "version", + VersionCodename: "codename", + BuildID: "build-id", + ImageID: "image-id", + ImageVersion: "image-version", + Variant: "variant", + VariantID: "variant-id", + HomeURL: "https://example.org/os", + SupportURL: "https://example.org/os/support", + BugReportURL: "https://example.org/os/bugs", + PrivacyPolicyURL: "https://example.org/os/privacy", + CPEName: "os-cpe", + SupportEnd: "now", + }, + }, + Relationships: nil, + Source: source.Description{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + Metadata: source.FileSourceMetadata{ + Path: "/some-file-source-path", + Digests: []file.Digest{ + { + Algorithm: "sha1", + Value: "d34db33f", + }, + }, + MIMEType: "file/zip", + }, + }, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "this-version", + }, + } + + buf := &bytes.Buffer{} + err := encoder(buf, s) + require.NoError(t, err) + + got, err := decoder(buf) + require.NoError(t, err) + + require.Equal(t, s, *got) +} diff --git a/syft/formats/syftjson/format_test.go b/syft/formats/syftjson/format_test.go new file mode 100644 index 000000000..4ef7a21a1 --- /dev/null +++ b/syft/formats/syftjson/format_test.go @@ -0,0 +1,33 @@ +package syftjson + +import ( + "testing" + + "github.com/anchore/syft/internal" +) + +func TestFormat(t *testing.T) { + tests := []struct { + name string + version string + }{ + { + name: "default version should use latest internal version", + version: "", + }, + } + + for _, c := range tests { + c := c + t.Run(c.name, func(t *testing.T) { + sbomFormat := Format() + if sbomFormat.ID() != ID { + t.Errorf("expected ID %q, got %q", ID, sbomFormat.ID()) + } + + if sbomFormat.Version() != internal.JSONSchemaVersion { + t.Errorf("expected version %q, got %q", c.version, sbomFormat.Version()) + } + }) + } +} diff --git a/syft/formats/syftjson/model/file.go b/syft/formats/syftjson/model/file.go index 757a29315..5827b3184 100644 --- a/syft/formats/syftjson/model/file.go +++ b/syft/formats/syftjson/model/file.go @@ -2,6 +2,7 @@ package model import ( "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/license" ) type File struct { @@ -10,6 +11,7 @@ type File struct { Metadata *FileMetadataEntry `json:"metadata,omitempty"` Contents string `json:"contents,omitempty"` Digests []file.Digest `json:"digests,omitempty"` + Licenses []FileLicense `json:"licenses,omitempty"` } type FileMetadataEntry struct { @@ -21,3 +23,16 @@ type FileMetadataEntry struct { MIMEType string `json:"mimeType"` Size int64 `json:"size"` } + +type FileLicense struct { + Value string `json:"value"` + SPDXExpression string `json:"spdxExpression"` + Type license.Type `json:"type"` + Evidence *FileLicenseEvidence `json:"evidence,omitempty"` +} + +type FileLicenseEvidence struct { + Confidence int `json:"confidence"` + Offset int `json:"offset"` + Extent int `json:"extent"` +} diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index 61bb2efe4..11674865f 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -41,7 +41,7 @@ } }, { - "id": "db4abfe497c180d3", + "id": "ad5013466727018f", "name": "package-2", "version": "2.0.1", "type": "deb", diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index f83789be5..10a5cc12c 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -36,7 +36,7 @@ } }, { - "id": "9fd0b9f41034991d", + "id": "aa0ca2c331576dfd", "name": "package-2", "version": "2.0.1", "type": "deb", diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index 518a90ab5..ba156fb88 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -37,7 +37,7 @@ } }, { - "id": "958443e2d9304af4", + "id": "f27313b22a5ba330", "name": "package-2", "version": "2.0.1", "type": "deb", diff --git a/syft/formats/syftjson/to_format_model.go b/syft/formats/syftjson/to_format_model.go index 2cafddf14..1cfda4618 100644 --- a/syft/formats/syftjson/to_format_model.go +++ b/syft/formats/syftjson/to_format_model.go @@ -106,12 +106,31 @@ func toFile(s sbom.SBOM) []model.File { contents = contentsForLocation } + var licenses []model.FileLicense + for _, l := range artifacts.FileLicenses[coordinates] { + var evidence *model.FileLicenseEvidence + if e := l.LicenseEvidence; e != nil { + evidence = &model.FileLicenseEvidence{ + Confidence: e.Confidence, + Offset: e.Offset, + Extent: e.Extent, + } + } + licenses = append(licenses, model.FileLicense{ + Value: l.Value, + SPDXExpression: l.SPDXExpression, + Type: l.Type, + Evidence: evidence, + }) + } + results = append(results, model.File{ ID: string(coordinates.ID()), Location: coordinates, Metadata: toFileMetadataEntry(coordinates, metadata), Digests: digests, Contents: contents, + Licenses: licenses, }) } diff --git a/syft/formats/syftjson/to_syft_model.go b/syft/formats/syftjson/to_syft_model.go index 419cf3ed4..990a33e27 100644 --- a/syft/formats/syftjson/to_syft_model.go +++ b/syft/formats/syftjson/to_syft_model.go @@ -34,6 +34,8 @@ func toSyftModel(doc model.Document) (*sbom.SBOM, error) { Packages: catalog, FileMetadata: fileArtifacts.FileMetadata, FileDigests: fileArtifacts.FileDigests, + FileContents: fileArtifacts.FileContents, + FileLicenses: fileArtifacts.FileLicenses, LinuxDistribution: toSyftLinuxRelease(doc.Distro), }, Source: *toSyftSourceData(doc.Source), @@ -66,6 +68,8 @@ func toSyftFiles(files []model.File) sbom.Artifacts { ret := sbom.Artifacts{ FileMetadata: make(map[file.Coordinates]file.Metadata), FileDigests: make(map[file.Coordinates][]file.Digest), + FileContents: make(map[file.Coordinates]string), + FileLicenses: make(map[file.Coordinates][]file.License), } for _, f := range files { @@ -100,6 +104,27 @@ func toSyftFiles(files []model.File) sbom.Artifacts { Value: d.Value, }) } + + if f.Contents != "" { + ret.FileContents[coord] = f.Contents + } + + for _, l := range f.Licenses { + var evidence *file.LicenseEvidence + if e := l.Evidence; e != nil { + evidence = &file.LicenseEvidence{ + Confidence: e.Confidence, + Offset: e.Offset, + Extent: e.Extent, + } + } + ret.FileLicenses[coord] = append(ret.FileLicenses[coord], file.License{ + Value: l.Value, + SPDXExpression: l.SPDXExpression, + Type: l.Type, + LicenseEvidence: evidence, + }) + } } return ret diff --git a/syft/formats/syftjson/to_syft_model_test.go b/syft/formats/syftjson/to_syft_model_test.go index 5600ec155..c4a6c0348 100644 --- a/syft/formats/syftjson/to_syft_model_test.go +++ b/syft/formats/syftjson/to_syft_model_test.go @@ -312,6 +312,8 @@ func Test_toSyftFiles(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + tt.want.FileContents = make(map[file.Coordinates]string) + tt.want.FileLicenses = make(map[file.Coordinates][]file.License) assert.Equal(t, tt.want, toSyftFiles(tt.files)) }) } diff --git a/syft/internal/fileresolver/container_image_all_layers_test.go b/syft/internal/fileresolver/container_image_all_layers_test.go index 7fb04d56b..a76082d85 100644 --- a/syft/internal/fileresolver/container_image_all_layers_test.go +++ b/syft/internal/fileresolver/container_image_all_layers_test.go @@ -1,9 +1,7 @@ package fileresolver import ( - "fmt" "io" - "runtime" "sort" "testing" @@ -523,11 +521,6 @@ func Test_imageAllLayersResolver_resolvesLinks(t *testing.T) { func TestAllLayersResolver_AllLocations(t *testing.T) { img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted") - arch := "x86_64" - if runtime.GOARCH == "arm64" { - arch = "aarch64" - } - resolver, err := NewFromContainerImageAllLayers(img) assert.NoError(t, err) @@ -638,9 +631,9 @@ func TestAllLayersResolver_AllLocations(t *testing.T) { "/lib/apk/db/triggers", "/lib/apk/exec", "/lib/firmware", - fmt.Sprintf("/lib/ld-musl-%s.so.1", arch), + "/lib/ld-musl-x86_64.so.1", "/lib/libapk.so.3.12.0", - fmt.Sprintf("/lib/libc.musl-%s.so.1", arch), + "/lib/libc.musl-x86_64.so.1", "/lib/libcrypto.so.3", "/lib/libssl.so.3", "/lib/libz.so.1", diff --git a/syft/internal/fileresolver/directory_indexer.go b/syft/internal/fileresolver/directory_indexer.go index c9b5567a9..b6432f000 100644 --- a/syft/internal/fileresolver/directory_indexer.go +++ b/syft/internal/fileresolver/directory_indexer.go @@ -349,9 +349,13 @@ func (r directoryIndexer) addSymlinkToIndex(p string, info os.FileInfo) (string, } if filepath.IsAbs(linkTarget) { + linkTarget = filepath.Clean(linkTarget) // if the link is absolute (e.g, /bin/ls -> /bin/busybox) we need to - // resolve relative to the root of the base directory - linkTarget = filepath.Join(r.base, filepath.Clean(linkTarget)) + // resolve relative to the root of the base directory, if it is not already + // prefixed with a volume name + if filepath.VolumeName(linkTarget) == "" { + linkTarget = filepath.Join(r.base, filepath.Clean(linkTarget)) + } } else { // if the link is not absolute (e.g, /dev/stderr -> fd/2 ) we need to // resolve it relative to the directory in question (e.g. resolve to diff --git a/syft/internal/fileresolver/test-fixtures/image-files-deleted/Dockerfile b/syft/internal/fileresolver/test-fixtures/image-files-deleted/Dockerfile index 5c5755194..10894f948 100644 --- a/syft/internal/fileresolver/test-fixtures/image-files-deleted/Dockerfile +++ b/syft/internal/fileresolver/test-fixtures/image-files-deleted/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.17.1 as tools +FROM alpine:3.17.1@sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0 as tools FROM scratch COPY --from=tools /bin /bin COPY --from=tools /lib /lib diff --git a/syft/internal/fileresolver/unindexed_directory.go b/syft/internal/fileresolver/unindexed_directory.go index 31e888913..68fdd7965 100644 --- a/syft/internal/fileresolver/unindexed_directory.go +++ b/syft/internal/fileresolver/unindexed_directory.go @@ -6,6 +6,7 @@ import ( "io/fs" "os" "path" + "slices" "sort" "strings" "time" @@ -13,7 +14,6 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/mitchellh/go-homedir" "github.com/spf13/afero" - "golang.org/x/exp/slices" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/file" diff --git a/syft/lib.go b/syft/lib.go index 32f010d02..d8ef7617d 100644 --- a/syft/lib.go +++ b/syft/lib.go @@ -92,8 +92,7 @@ func CatalogPackages(src source.Source, cfg cataloger.Config) (*pkg.Collection, } func removeRelationshipsByID(relationships []artifact.Relationship, id artifact.ID) []artifact.Relationship { - // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating - filtered := relationships[:0] + var filtered []artifact.Relationship for _, r := range relationships { if r.To.ID() != id && r.From.ID() != id { filtered = append(filtered, r) diff --git a/syft/lib_test.go b/syft/lib_test.go new file mode 100644 index 000000000..0e2ca8a8e --- /dev/null +++ b/syft/lib_test.go @@ -0,0 +1,42 @@ +package syft + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" +) + +func Test_removeRelationshipsByID(t *testing.T) { + p1 := pkg.Package{} + p1.OverrideID("1") + + p2 := pkg.Package{} + p2.OverrideID("2") + + p3 := pkg.Package{} + p3.OverrideID("3") + + rel := func(pkgs ...pkg.Package) (out []artifact.Relationship) { + for _, p := range pkgs { + out = append(out, artifact.Relationship{ + From: p, + To: p, + Type: artifact.OwnershipByFileOverlapRelationship, + }) + } + return + } + + relationships := rel(p1, p2, p3) + + for _, r := range relationships { + if r.From.ID() == "1" || r.From.ID() == "2" { + relationships = removeRelationshipsByID(relationships, r.From.ID()) + } + } + + require.Equal(t, rel(p3), relationships) +} diff --git a/syft/pkg/cataloger/binary/cataloger_test.go b/syft/pkg/cataloger/binary/cataloger_test.go index c66cb88db..188069e8b 100644 --- a/syft/pkg/cataloger/binary/cataloger_test.go +++ b/syft/pkg/cataloger/binary/cataloger_test.go @@ -667,6 +667,18 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { Metadata: metadata("nginx-binary"), }, }, + { + name: "positive-bash-5.2.15", + fixtureDir: "test-fixtures/classifiers/positive/bash-5.2.15", + expected: pkg.Package{ + Name: "bash", + Version: "5.2.15", + Type: "binary", + PURL: "pkg:generic/bash@5.2.15", + Locations: locations("bash"), + Metadata: metadata("bash-binary"), + }, + }, } for _, test := range tests { diff --git a/syft/pkg/cataloger/binary/default_classifiers.go b/syft/pkg/cataloger/binary/default_classifiers.go index 93ed25d30..24cb43219 100644 --- a/syft/pkg/cataloger/binary/default_classifiers.go +++ b/syft/pkg/cataloger/binary/default_classifiers.go @@ -269,6 +269,20 @@ var defaultClassifiers = []classifier{ cpe.Must("cpe:2.3:a:nginx:nginx:*:*:*:*:*:*:*:*"), }, }, + { + Class: "bash-binary", + FileGlob: "**/bash", + EvidenceMatcher: fileContentsVersionMatcher( + // @(#)Bash version 5.2.15(1) release GNU + // @(#)Bash version 5.2.0(1) alpha GNU + // @(#)Bash version 5.2.0(1) beta GNU + // @(#)Bash version 5.2.0(1) rc4 GNU + `(?m)@\(#\)Bash version (?P[0-9]+\.[0-9]+\.[0-9]+)\([0-9]\) [a-z0-9]+ GNU`, + ), + Package: "bash", + PURL: mustPURL("pkg:generic/bash@version"), + CPEs: singleCPE("cpe:2.3:a:gnu:bash:*:*:*:*:*:*:*:*"), + }, } // in both binaries and shared libraries, the version pattern is [NUL]3.11.2[NUL] diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/positive/bash-5.2.15/bash b/syft/pkg/cataloger/binary/test-fixtures/classifiers/positive/bash-5.2.15/bash new file mode 100644 index 000000000..17c8fc2a0 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/classifiers/positive/bash-5.2.15/bash @@ -0,0 +1 @@ +@(#)Bash version 5.2.15(1) release GNU diff --git a/syft/pkg/cataloger/binary/test-fixtures/image-busybox/Dockerfile b/syft/pkg/cataloger/binary/test-fixtures/image-busybox/Dockerfile index 94b54d2f4..5af8c83d1 100644 --- a/syft/pkg/cataloger/binary/test-fixtures/image-busybox/Dockerfile +++ b/syft/pkg/cataloger/binary/test-fixtures/image-busybox/Dockerfile @@ -1 +1 @@ -FROM busybox:1.35 \ No newline at end of file +FROM busybox:1.35@sha256:7ae8447f3a7f5bccaa765926f25fc038e425cf1b2be6748727bbea9a13102094 diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 2d3580024..c02c7e233 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -19,6 +19,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/dotnet" "github.com/anchore/syft/syft/pkg/cataloger/elixir" "github.com/anchore/syft/syft/pkg/cataloger/erlang" + "github.com/anchore/syft/syft/pkg/cataloger/githubactions" "github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/haskell" "github.com/anchore/syft/syft/pkg/cataloger/java" @@ -74,6 +75,8 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger { dotnet.NewDotnetPortableExecutableCataloger(), elixir.NewMixLockCataloger(), erlang.NewRebarLockCataloger(), + githubactions.NewActionUsageCataloger(), + githubactions.NewWorkflowUsageCataloger(), golang.NewGoModFileCataloger(cfg.Golang), golang.NewGoModuleBinaryCataloger(cfg.Golang), haskell.NewHackageCataloger(), @@ -110,6 +113,8 @@ func AllCatalogers(cfg Config) []pkg.Cataloger { dotnet.NewDotnetPortableExecutableCataloger(), elixir.NewMixLockCataloger(), erlang.NewRebarLockCataloger(), + githubactions.NewActionUsageCataloger(), + githubactions.NewWorkflowUsageCataloger(), golang.NewGoModFileCataloger(cfg.Golang), golang.NewGoModuleBinaryCataloger(cfg.Golang), haskell.NewHackageCataloger(), diff --git a/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go b/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go index bc62d3909..bc1e5fc62 100644 --- a/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go +++ b/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go @@ -183,6 +183,11 @@ var defaultCandidateAdditions = buildCandidateLookup( candidateKey{PkgName: "pip"}, candidateAddition{AdditionalVendors: []string{"pypa"}}, }, + { + pkg.PythonPkg, + candidateKey{PkgName: "Django"}, + candidateAddition{AdditionalVendors: []string{"djangoproject"}}, + }, // Alpine packages { pkg.ApkPkg, diff --git a/syft/pkg/cataloger/common/cpe/dictionary/data/cpe-index.json b/syft/pkg/cataloger/common/cpe/dictionary/data/cpe-index.json index a47e39ca6..4d021dc1e 100644 --- a/syft/pkg/cataloger/common/cpe/dictionary/data/cpe-index.json +++ b/syft/pkg/cataloger/common/cpe/dictionary/data/cpe-index.json @@ -283,6 +283,7 @@ "360class.jansenhm": "cpe:2.3:a:360class.jansenhm_project:360class.jansenhm:*:*:*:*:*:node.js:*:*", "626": "cpe:2.3:a:626_project:626:*:*:*:*:*:node.js:*:*", "@actions/core": "cpe:2.3:a:toolkit_project:toolkit:*:*:*:*:*:node.js:*:*", + "@antfu/utils": "cpe:2.3:a:antfu:utils:*:*:*:*:*:node.js:*:*", "@awsui/components-react": "cpe:2.3:a:amazon:awsui\\/components-react:*:*:*:*:*:node.js:*:*", "@azure/ms-rest-nodeauth": "cpe:2.3:a:microsoft:ms-rest-nodeauth:*:*:*:*:*:node.js:*:*", "@backstage/plugin-auth-backend": "cpe:2.3:a:linuxfoundation:auth_backend:*:*:*:*:*:node.js:*:*", @@ -474,6 +475,7 @@ "fast-csv": "cpe:2.3:a:c2fo:fast-csv:*:*:*:*:*:node.js:*:*", "fast-http": "cpe:2.3:a:fast-http_project:fast-http:*:*:*:*:*:node.js:*:*", "fast-http-cli": "cpe:2.3:a:fast-http-cli_project:fast-http-cli:*:*:*:*:*:node.js:*:*", + "fastest-json-copy": "cpe:2.3:a:fastest-json-copy_project:fastest-json-copy:*:*:*:*:*:node.js:*:*", "ffmepg": "cpe:2.3:a:ffmepg_project:ffmepg:*:*:*:*:*:node.js:*:*", "ffmpegdotjs": "cpe:2.3:a:ffmpegdotjs_project:ffmpegdotjs:*:*:*:*:*:node.js:*:*", "fibjs": "cpe:2.3:a:fibjs_project:fibjs:*:*:*:*:*:node.js:*:*", @@ -807,6 +809,7 @@ "promise-probe": "cpe:2.3:a:promise-probe_project:promise-probe:*:*:*:*:*:node.js:*:*", "promisehelpers": "cpe:2.3:a:yola:promisehelpers:*:*:*:*:*:node.js:*:*", "property-expr": "cpe:2.3:a:property-expr_project:property-expr:*:*:*:*:*:node.js:*:*", + "proxy": "cpe:2.3:a:proxy_project:proxy:*:*:*:*:*:node.js:*:*", "proxy.js": "cpe:2.3:a:proxy.js_project:proxy.js:*:*:*:*:*:node.js:*:*", "ps-kill": "cpe:2.3:a:ps-kill_project:ps-kill:*:*:*:*:*:node.js:*:*", "ps-visitor": "cpe:2.3:a:ps-visitor_project:ps-visitor:*:*:*:*:*:node.js:*:*", @@ -1057,6 +1060,7 @@ "pipreqs": "cpe:2.3:a:pipreqs_project:pipreqs:*:*:*:*:*:python:*:*", "proxy.py": "cpe:2.3:a:proxy.py_project:proxy.py:*:*:*:*:*:*:*:*", "py-bcrypt": "cpe:2.3:a:python:py-bcrypt:*:*:*:*:*:*:*:*", + "py-xml": "cpe:2.3:a:py-xml_project:py-xml:*:*:*:*:*:python:*:*", "py7zr": "cpe:2.3:a:py7zr_project:py7zr:*:*:*:*:*:python:*:*", "pybluemonday": "cpe:2.3:a:python:pybluemonday:*:*:*:*:*:*:*:*", "pycryptodome": "cpe:2.3:a:python:pycryptodome:*:*:*:*:*:*:*:*", @@ -1080,6 +1084,7 @@ "spacy": "cpe:2.3:a:explosion:spacy:*:*:*:*:*:python:*:*", "sqlparse": "cpe:2.3:a:sqlparse_project:sqlparse:*:*:*:*:*:python:*:*", "tkvideoplayer": "cpe:2.3:a:python:tkvideoplayer:*:*:*:*:*:*:*:*", + "togglee": "cpe:2.3:a:togglee:togglee:*:*:*:*:*:pypi:*:*", "urllib3": "cpe:2.3:a:python:urllib3:*:*:*:*:*:*:*:*", "validators": "cpe:2.3:a:validators_project:validators:*:*:*:*:*:python:*:*", "vault-cli": "cpe:2.3:a:vault-cli_project:vault-cli:*:*:*:*:*:python:*:*", @@ -1144,6 +1149,7 @@ "papercrop": "cpe:2.3:a:papercrop_project:papercrop:*:*:*:*:*:ruby:*:*", "paranoid2": "cpe:2.3:a:anjlab:paranoid2:*:*:*:*:*:ruby:*:*", "paratrooper-pingdom": "cpe:2.3:a:tobias_maier:paratrooper-pingdom:*:*:-:*:-:ruby:*:*", + "pdf_info": "cpe:2.3:a:newspaperclub:pdf_info:*:*:*:*:*:ruby:*:*", "pdfkit": "cpe:2.3:a:pdfkit_project:pdfkit:*:*:*:*:*:ruby:*:*", "point-cli": "cpe:2.3:a:point-cli_project:point-cli:*:*:*:*:*:ruby:*:*", "private_address_check": "cpe:2.3:a:private_address_check_project:private_address_check:*:*:*:*:*:ruby:*:*", diff --git a/syft/pkg/cataloger/common/cpe/dictionary/index-generator/generate.go b/syft/pkg/cataloger/common/cpe/dictionary/index-generator/generate.go index 30ba79ffc..c0de14ba5 100644 --- a/syft/pkg/cataloger/common/cpe/dictionary/index-generator/generate.go +++ b/syft/pkg/cataloger/common/cpe/dictionary/index-generator/generate.go @@ -7,10 +7,10 @@ import ( "fmt" "io" "log" + "slices" "strings" "github.com/facebookincubator/nvdtools/wfn" - "golang.org/x/exp/slices" "github.com/anchore/syft/syft/pkg/cataloger/common/cpe/dictionary" ) diff --git a/syft/pkg/cataloger/common/cpe/generate_test.go b/syft/pkg/cataloger/common/cpe/generate_test.go index 939c2d3eb..2e6b131d7 100644 --- a/syft/pkg/cataloger/common/cpe/generate_test.go +++ b/syft/pkg/cataloger/common/cpe/generate_test.go @@ -896,6 +896,14 @@ func TestCandidateVendor(t *testing.T) { }, expected: []string{"apache"}, }, + { + name: "Django", + p: pkg.Package{ + Name: "Django", + Type: pkg.PythonPkg, + }, + expected: []string{"djangoproject" /* <-- known good names | default guess --> */, "Django"}, + }, } for _, test := range tests { diff --git a/syft/pkg/cataloger/common/cpe/java.go b/syft/pkg/cataloger/common/cpe/java.go index 6de454c06..c8bde9f77 100644 --- a/syft/pkg/cataloger/common/cpe/java.go +++ b/syft/pkg/cataloger/common/cpe/java.go @@ -1,6 +1,7 @@ package cpe import ( + "sort" "strings" "github.com/scylladb/go-set/strset" @@ -21,16 +22,16 @@ var ( "be", } - primaryJavaManifestGroupIDFields = []string{ + PrimaryJavaManifestGroupIDFields = []string{ + "Bundle-SymbolicName", "Extension-Name", "Specification-Vendor", "Implementation-Vendor", - "Bundle-SymbolicName", "Implementation-Vendor-Id", "Implementation-Title", "Bundle-Activator", } - secondaryJavaManifestGroupIDFields = []string{ + SecondaryJavaManifestGroupIDFields = []string{ "Automatic-Module-Name", "Main-Class", "Package", @@ -168,7 +169,7 @@ func artifactIDFromJavaPackage(p pkg.Package) string { } artifactID := strings.TrimSpace(metadata.PomProperties.ArtifactID) - if startsWithTopLevelDomain(artifactID) && len(strings.Split(artifactID, ".")) > 1 { + if looksLikeGroupID(artifactID) && len(strings.Split(artifactID, ".")) > 1 { // there is a strong indication that the artifact ID is really a group ID, don't use it return "" } @@ -181,13 +182,16 @@ func GroupIDsFromJavaPackage(p pkg.Package) (groupIDs []string) { return nil } - return GroupIDsFromJavaMetadata(metadata) + return GroupIDsFromJavaMetadata(p.Name, metadata) } -func GroupIDsFromJavaMetadata(metadata pkg.JavaMetadata) (groupIDs []string) { +// GroupIDsFromJavaMetadata returns the possible group IDs for a Java package +// This function is similar to GroupIDFromJavaPackage, but returns all possible group IDs and is less strict +// It is used as a way to generate possible candidates for CPE matching. +func GroupIDsFromJavaMetadata(pkgName string, metadata pkg.JavaMetadata) (groupIDs []string) { groupIDs = append(groupIDs, groupIDsFromPomProperties(metadata.PomProperties)...) groupIDs = append(groupIDs, groupIDsFromPomProject(metadata.PomProject)...) - groupIDs = append(groupIDs, groupIDsFromJavaManifest(metadata.Manifest)...) + groupIDs = append(groupIDs, groupIDsFromJavaManifest(pkgName, metadata.Manifest)...) return groupIDs } @@ -241,13 +245,17 @@ func addGroupIDsFromGroupIDsAndArtifactID(groupID, artifactID string) (groupIDs return groupIDs } -func groupIDsFromJavaManifest(manifest *pkg.JavaManifest) []string { +func groupIDsFromJavaManifest(pkgName string, manifest *pkg.JavaManifest) []string { + if groupID, ok := DefaultArtifactIDToGroupID[pkgName]; ok { + return []string{groupID} + } + if manifest == nil { return nil } // try the common manifest fields first for a set of candidates - groupIDs := getManifestFieldGroupIDs(manifest, primaryJavaManifestGroupIDFields) + groupIDs := GetManifestFieldGroupIDs(manifest, PrimaryJavaManifestGroupIDFields) if len(groupIDs) != 0 { return groupIDs @@ -258,10 +266,10 @@ func groupIDsFromJavaManifest(manifest *pkg.JavaManifest) []string { // for more info see pkg:maven/commons-io/commons-io@2.8.0 within cloudbees/cloudbees-core-mm:2.263.4.2 // at /usr/share/jenkins/jenkins.war:WEB-INF/plugins/analysis-model-api.hpi:WEB-INF/lib/commons-io-2.8.0.jar // as well as the ant package from cloudbees/cloudbees-core-mm:2.277.2.4-ra. - return getManifestFieldGroupIDs(manifest, secondaryJavaManifestGroupIDFields) + return GetManifestFieldGroupIDs(manifest, SecondaryJavaManifestGroupIDFields) } -func getManifestFieldGroupIDs(manifest *pkg.JavaManifest, fields []string) (groupIDs []string) { +func GetManifestFieldGroupIDs(manifest *pkg.JavaManifest, fields []string) (groupIDs []string) { if manifest == nil { return nil } @@ -280,6 +288,7 @@ func getManifestFieldGroupIDs(manifest *pkg.JavaManifest, fields []string) (grou } } } + sort.Strings(groupIDs) return groupIDs } @@ -298,3 +307,7 @@ func removeOSCIDirectives(groupID string) string { func startsWithTopLevelDomain(value string) bool { return internal.HasAnyOfPrefixes(value, domains...) } + +func looksLikeGroupID(value string) bool { + return strings.Contains(value, ".") +} diff --git a/syft/pkg/cataloger/common/cpe/java_groupid_map.go b/syft/pkg/cataloger/common/cpe/java_groupid_map.go new file mode 100644 index 000000000..634b4dce0 --- /dev/null +++ b/syft/pkg/cataloger/common/cpe/java_groupid_map.go @@ -0,0 +1,73 @@ +package cpe + +var DefaultArtifactIDToGroupID = map[string]string{ + "ant": "org.apache.ant", + "ant-antlr": "org.apache.ant", + "ant-antunit": "org.apache.ant", + "ant-apache-bcel": "org.apache.ant", + "ant-apache-bsf": "org.apache.ant", + "ant-apache-log4j": "org.apache.ant", + "ant-apache-oro": "org.apache.ant", + "ant-apache-regexp": "org.apache.ant", + "ant-apache-resolver": "org.apache.ant", + "ant-apache-xalan2": "org.apache.ant", + "ant-commons-logging": "org.apache.ant", + "ant-commons-net": "org.apache.ant", + "ant-compress": "org.apache.ant", + "ant-dotnet": "org.apache.ant", + "ant-imageio": "org.apache.ant", + "ant-jai": "org.apache.ant", + "ant-jakartamail": "org.apache.ant", + "ant-javamail": "org.apache.ant", + "ant-jdepend": "org.apache.ant", + "ant-jmf": "org.apache.ant", + "ant-jsch": "org.apache.ant", + "ant-junit": "org.apache.ant", + "ant-junit4": "org.apache.ant", + "ant-junitlauncher": "org.apache.ant", + "ant-launcher": "org.apache.ant", + "ant-netrexx": "org.apache.ant", + "ant-nodeps": "org.apache.ant", + "ant-parent": "org.apache.ant", + "ant-starteam": "org.apache.ant", + "ant-stylebook": "org.apache.ant", + "ant-swing": "org.apache.ant", + "ant-testutil": "org.apache.ant", + "ant-trax": "org.apache.ant", + "ant-weblogic": "org.apache.ant", + "ant-xz": "org.apache.ant", + "commons-codec": "commons-codec", + "commons-logging": "commons-logging", // see e.g. https://mvnrepository.com/artifact/commons-logging/commons-logging/1.1.1 + "okhttp": "com.squareup.okhttp3", + "okio": "com.squareup.okio", + "spring": "org.springframework", + "spring-amqp": "org.springframework.amqp", + "spring-batch-core": "org.springframework.batch", + "spring-beans": "org.springframework", + "spring-boot": "org.springframework.boot", + "spring-boot-starter-web": "org.springframework.boot", + "spring-boot-starter-webflux": "org.springframework.boot", + "spring-cloud-function-context": "org.springframework.cloud", + "spring-cloud-function-parent": "org.springframework.cloud", + "spring-cloud-gateway": "org.springframework.cloud", + "spring-cloud-openfeign-core": "org.springframework.cloud", + "spring-cloud-task-dependencies": "org.springframework.cloud", + "spring-core": "org.springframework", + "spring-data-jpa": "org.springframework.data", + "spring-data-mongodb": "org.springframework.data", + "spring-data-rest-core": "org.springframework.data", + "spring-expression": "org.springframework", + "spring-integration-zip": "org.springframework.integration", + "spring-oxm": "org.springframework", + "spring-security-core": "org.springframework.security", + "spring-security-config": "org.springframework.security", + "spring-security-oauth": "org.springframework.security.oauth", + "spring-security-oauth-parent": "org.springframework.security.oauth", + "spring-security-oauth2-client": "org.springframework.security", + "spring-session-core": "org.springframework.session", + "spring-vault-core": "org.springframework.vault", + "spring-web": "org.springframework", + "spring-webflow": "org.springframework.webflow", + "spring-webflux": "org.springframework", + "spring-webmvc": "org.springframework", +} diff --git a/syft/pkg/cataloger/common/cpe/java_test.go b/syft/pkg/cataloger/common/cpe/java_test.go index c27e2ac5b..9d75014ac 100644 --- a/syft/pkg/cataloger/common/cpe/java_test.go +++ b/syft/pkg/cataloger/common/cpe/java_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/pkg" ) @@ -427,3 +428,38 @@ func Test_vendorsFromJavaManifestNames(t *testing.T) { }) } } + +func Test_groupIDsFromJavaManifest(t *testing.T) { + tests := []struct { + name string + manifest pkg.JavaManifest + expected []string + }{ + { + name: "spring-security-core", + manifest: pkg.JavaManifest{}, + expected: []string{"org.springframework.security"}, + }, + { + name: "spring-web", + manifest: pkg.JavaManifest{}, + expected: []string{"org.springframework"}, + }, + { + name: "spring-foo", + manifest: pkg.JavaManifest{ + Main: map[string]string{ + "Implementation-Vendor": "org.foo", + }, + }, + expected: []string{"org.foo"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := groupIDsFromJavaManifest(test.name, &test.manifest) + require.Equal(t, test.expected, got) + }) + } +} diff --git a/syft/pkg/cataloger/config.go b/syft/pkg/cataloger/config.go index d0a64d83d..df3b63978 100644 --- a/syft/pkg/cataloger/config.go +++ b/syft/pkg/cataloger/config.go @@ -18,6 +18,16 @@ type Config struct { ExcludeBinaryOverlapByOwnership bool } +func DefaultConfig() Config { + return Config{ + Search: DefaultSearchConfig(), + Parallelism: 1, + LinuxKernel: kernel.DefaultLinuxCatalogerConfig(), + Python: python.DefaultCatalogerConfig(), + ExcludeBinaryOverlapByOwnership: true, + } +} + func (c Config) Java() java.Config { return java.Config{ SearchUnindexedArchives: c.Search.IncludeUnindexedArchives, diff --git a/syft/pkg/cataloger/cpp/package.go b/syft/pkg/cataloger/cpp/package.go index dbbdd0b90..b093c928d 100644 --- a/syft/pkg/cataloger/cpp/package.go +++ b/syft/pkg/cataloger/cpp/package.go @@ -8,23 +8,67 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newConanfilePackage(m pkg.ConanMetadata, locations ...file.Location) *pkg.Package { - fields := strings.Split(strings.TrimSpace(m.Ref), "/") - if len(fields) < 2 { - return nil +type conanRef struct { + Name string + Version string + User string + Channel string + Revision string + Timestamp string +} + +func splitConanRef(ref string) *conanRef { + // Conan ref format is: + // pkg/0.1@user/channel#rrev%timestamp + // This method is based on conan's ref.loads method: + // https://github.com/conan-io/conan/blob/release/2.0/conans/model/recipe_ref.py#L93C21-L93C21 + + var cref conanRef + + // timestamp + tokens := strings.Split(ref, "%") + text := tokens[0] + if len(tokens) == 2 { + cref.Timestamp = tokens[1] } - pkgName, pkgVersion := fields[0], fields[1] + // revision + tokens = strings.Split(text, "#") + ref = tokens[0] + if len(tokens) == 2 { + cref.Revision = tokens[1] + } - if pkgName == "" || pkgVersion == "" { + // name and version are always given + tokens = strings.Split(ref, "@") + nameAndVersion := strings.Split(tokens[0], "/") + if len(nameAndVersion) < 2 || nameAndVersion[0] == "" || nameAndVersion[1] == "" { + return nil + } + cref.Name = nameAndVersion[0] + cref.Version = nameAndVersion[1] + // user and channel + if len(tokens) == 2 && tokens[1] != "" { + tokens = strings.Split(tokens[1], "/") + if len(tokens) == 2 { + cref.User = tokens[0] + cref.Channel = tokens[1] + } + } + return &cref +} + +func newConanfilePackage(m pkg.ConanMetadata, locations ...file.Location) *pkg.Package { + ref := splitConanRef(m.Ref) + if ref == nil { return nil } p := pkg.Package{ - Name: pkgName, - Version: pkgVersion, + Name: ref.Name, + Version: ref.Version, Locations: file.NewLocationSet(locations...), - PURL: packageURL(pkgName, pkgVersion), + PURL: packageURL(ref), Language: pkg.CPP, Type: pkg.ConanPkg, MetadataType: pkg.ConanMetadataType, @@ -37,22 +81,16 @@ func newConanfilePackage(m pkg.ConanMetadata, locations ...file.Location) *pkg.P } func newConanlockPackage(m pkg.ConanLockMetadata, locations ...file.Location) *pkg.Package { - fields := strings.Split(strings.Split(m.Ref, "@")[0], "/") - if len(fields) < 2 { - return nil - } - - pkgName, pkgVersion := fields[0], fields[1] - - if pkgName == "" || pkgVersion == "" { + ref := splitConanRef(m.Ref) + if ref == nil { return nil } p := pkg.Package{ - Name: pkgName, - Version: pkgVersion, + Name: ref.Name, + Version: ref.Version, Locations: file.NewLocationSet(locations...), - PURL: packageURL(pkgName, pkgVersion), + PURL: packageURL(ref), Language: pkg.CPP, Type: pkg.ConanPkg, MetadataType: pkg.ConanLockMetadataType, @@ -64,13 +102,20 @@ func newConanlockPackage(m pkg.ConanLockMetadata, locations ...file.Location) *p return &p } -func packageURL(name, version string) string { +func packageURL(ref *conanRef) string { + qualifiers := packageurl.Qualifiers{} + if ref.Channel != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "channel", + Value: ref.Channel, + }) + } return packageurl.NewPackageURL( packageurl.TypeConan, - "", - name, - version, - nil, // TODO: no qualifiers (...yet) + ref.User, + ref.Name, + ref.Version, + qualifiers, "", ).ToString() } diff --git a/syft/pkg/cataloger/cpp/parse_conanfile.go b/syft/pkg/cataloger/cpp/parse_conanfile.go index f9ae172f3..bf60706db 100644 --- a/syft/pkg/cataloger/cpp/parse_conanfile.go +++ b/syft/pkg/cataloger/cpp/parse_conanfile.go @@ -36,7 +36,7 @@ func parseConanfile(_ file.Resolver, _ *generic.Environment, reader file.Locatio switch { case strings.Contains(line, "[requires]"): inRequirements = true - case strings.ContainsAny(line, "[]#"): + case strings.ContainsAny(line, "[]") || strings.HasPrefix(strings.TrimSpace(line), "#"): inRequirements = false } diff --git a/syft/pkg/cataloger/cpp/parse_conanfile_test.go b/syft/pkg/cataloger/cpp/parse_conanfile_test.go index bca49223a..93ba846a2 100644 --- a/syft/pkg/cataloger/cpp/parse_conanfile_test.go +++ b/syft/pkg/cataloger/cpp/parse_conanfile_test.go @@ -52,13 +52,13 @@ func TestParseConanfile(t *testing.T) { { Name: "spdlog", Version: "1.9.2", - PURL: "pkg:conan/spdlog@1.9.2", + PURL: "pkg:conan/my_user/spdlog@1.9.2?channel=my_channel", Locations: fixtureLocationSet, Language: pkg.CPP, Type: pkg.ConanPkg, MetadataType: pkg.ConanMetadataType, Metadata: pkg.ConanMetadata{ - Ref: "spdlog/1.9.2", + Ref: "spdlog/1.9.2@my_user/my_channel#1234567%%987654", }, }, { @@ -70,19 +70,19 @@ func TestParseConanfile(t *testing.T) { Type: pkg.ConanPkg, MetadataType: pkg.ConanMetadataType, Metadata: pkg.ConanMetadata{ - Ref: "sdl/2.0.20", + Ref: "sdl/2.0.20#1234567%%987654", }, }, { Name: "fltk", Version: "1.3.8", - PURL: "pkg:conan/fltk@1.3.8", + PURL: "pkg:conan/my_user/fltk@1.3.8?channel=my_channel", Locations: fixtureLocationSet, Language: pkg.CPP, Type: pkg.ConanPkg, MetadataType: pkg.ConanMetadataType, Metadata: pkg.ConanMetadata{ - Ref: "fltk/1.3.8", + Ref: "fltk/1.3.8@my_user/my_channel", }, }, } diff --git a/syft/pkg/cataloger/cpp/parse_conanlock.go b/syft/pkg/cataloger/cpp/parse_conanlock.go index 511000ea1..ef1b3b1a9 100644 --- a/syft/pkg/cataloger/cpp/parse_conanlock.go +++ b/syft/pkg/cataloger/cpp/parse_conanlock.go @@ -36,12 +36,24 @@ func parseConanlock(_ file.Resolver, _ *generic.Environment, reader file.Locatio if err := json.NewDecoder(reader).Decode(&cl); err != nil { return nil, nil, err } - for _, node := range cl.GraphLock.Nodes { + + // requires is a list of package indices. We first need to fill it, and then we can resolve the package + // in a second iteration + var indexToPkgMap = map[string]pkg.Package{} + + // we do not want to store the index list requires in the conan metadata, because it is not useful to have it in + // the SBOM. Instead, we will store it in a map and then use it to build the relationships + // maps pkg.ID to a list of indices + var parsedPkgRequires = map[artifact.ID][]string{} + + for idx, node := range cl.GraphLock.Nodes { metadata := pkg.ConanLockMetadata{ - Ref: node.Ref, - Options: parseOptions(node.Options), - Path: node.Path, - Context: node.Context, + Ref: node.Ref, + Options: parseOptions(node.Options), + Path: node.Path, + Context: node.Context, + PackageID: node.PackageID, + Prev: node.Prev, } p := newConanlockPackage( @@ -50,11 +62,28 @@ func parseConanlock(_ file.Resolver, _ *generic.Environment, reader file.Locatio ) if p != nil { - pkgs = append(pkgs, *p) + pk := *p + pkgs = append(pkgs, pk) + parsedPkgRequires[pk.ID()] = node.Requires + indexToPkgMap[idx] = pk } } - return pkgs, nil, nil + var relationships []artifact.Relationship + + for _, p := range pkgs { + requires := parsedPkgRequires[p.ID()] + for _, r := range requires { + // this is a pkg that package "p" depends on... make a relationship + relationships = append(relationships, artifact.Relationship{ + From: indexToPkgMap[r], + To: p, + Type: artifact.DependencyOfRelationship, + }) + } + } + + return pkgs, relationships, nil } func parseOptions(options string) map[string]string { diff --git a/syft/pkg/cataloger/cpp/parse_conanlock_test.go b/syft/pkg/cataloger/cpp/parse_conanlock_test.go index b699081de..8f6954701 100644 --- a/syft/pkg/cataloger/cpp/parse_conanlock_test.go +++ b/syft/pkg/cataloger/cpp/parse_conanlock_test.go @@ -12,6 +12,185 @@ import ( func TestParseConanlock(t *testing.T) { fixture := "test-fixtures/conan.lock" expected := []pkg.Package{ + { + Name: "mfast", + Version: "1.2.2", + PURL: "pkg:conan/my_user/mfast@1.2.2?channel=my_channel", + Locations: file.NewLocationSet(file.NewLocation(fixture)), + Language: pkg.CPP, + Type: pkg.ConanPkg, + MetadataType: pkg.ConanLockMetadataType, + Metadata: pkg.ConanLockMetadata{ + Ref: "mfast/1.2.2@my_user/my_channel#c6f6387c9b99780f0ee05e25f99d0f39", + Options: map[string]string{ + "fPIC": "True", + "shared": "False", + "with_sqlite3": "False", + "boost:addr2line_location": "/usr/bin/addr2line", + "boost:asio_no_deprecated": "False", + "boost:buildid": "None", + "boost:bzip2": "True", + "boost:debug_level": "0", + "boost:diagnostic_definitions": "False", + "boost:error_code_header_only": "False", + "boost:extra_b2_flags": "None", + "boost:fPIC": "True", + "boost:filesystem_no_deprecated": "False", + "boost:header_only": "False", + "boost:i18n_backend": "deprecated", + "boost:i18n_backend_iconv": "libc", + "boost:i18n_backend_icu": "False", + "boost:layout": "system", + "boost:lzma": "False", + "boost:magic_autolink": "False", + "boost:multithreading": "True", + "boost:namespace": "boost", + "boost:namespace_alias": "False", + "boost:numa": "True", + "boost:pch": "True", + "boost:python_executable": "None", + "boost:python_version": "None", + "boost:segmented_stacks": "False", + "boost:shared": "False", + "boost:system_no_deprecated": "False", + "boost:system_use_utf8": "False", + "boost:visibility": "hidden", + "boost:with_stacktrace_backtrace": "True", + "boost:without_atomic": "False", + "boost:without_chrono": "False", + "boost:without_container": "False", + "boost:without_context": "False", + "boost:without_contract": "False", + "boost:without_coroutine": "False", + "boost:without_date_time": "False", + "boost:without_exception": "False", + "boost:without_fiber": "False", + "boost:without_filesystem": "False", + "boost:without_graph": "False", + "boost:without_graph_parallel": "True", + "boost:without_iostreams": "False", + "boost:without_json": "False", + "boost:without_locale": "False", + "boost:without_log": "False", + "boost:without_math": "False", + "boost:without_mpi": "True", + "boost:without_nowide": "False", + "boost:without_program_options": "False", + "boost:without_python": "True", + "boost:without_random": "False", + "boost:without_regex": "False", + "boost:without_serialization": "False", + "boost:without_stacktrace": "False", + "boost:without_system": "False", + "boost:without_test": "False", + "boost:without_thread": "False", + "boost:without_timer": "False", + "boost:without_type_erasure": "False", + "boost:without_wave": "False", + "boost:zlib": "True", + "boost:zstd": "False", + "bzip2:build_executable": "True", + "bzip2:fPIC": "True", + "bzip2:shared": "False", + "libbacktrace:fPIC": "True", + "libbacktrace:shared": "False", + "tinyxml2:fPIC": "True", + "tinyxml2:shared": "False", + "zlib:fPIC": "True", + "zlib:shared": "False", + }, + Context: "host", + PackageID: "9d1f076b471417647c2022a78d5e2c1f834289ac", + Prev: "0ca9799450422cc55a92ccc6ffd57fba", + }, + }, + { + Name: "boost", + Version: "1.75.0", + PURL: "pkg:conan/boost@1.75.0", + Locations: file.NewLocationSet(file.NewLocation(fixture)), + Language: pkg.CPP, + Type: pkg.ConanPkg, + MetadataType: pkg.ConanLockMetadataType, + Metadata: pkg.ConanLockMetadata{ + Ref: "boost/1.75.0#a9c318f067216f900900e044e7af4ab1", + Options: map[string]string{ + "addr2line_location": "/usr/bin/addr2line", + "asio_no_deprecated": "False", + "buildid": "None", + "bzip2": "True", + "debug_level": "0", + "diagnostic_definitions": "False", + "error_code_header_only": "False", + "extra_b2_flags": "None", + "fPIC": "True", + "filesystem_no_deprecated": "False", + "header_only": "False", + "i18n_backend": "deprecated", + "i18n_backend_iconv": "libc", + "i18n_backend_icu": "False", + "layout": "system", + "lzma": "False", + "magic_autolink": "False", + "multithreading": "True", + "namespace": "boost", + "namespace_alias": "False", + "numa": "True", + "pch": "True", + "python_executable": "None", + "python_version": "None", + "segmented_stacks": "False", + "shared": "False", + "system_no_deprecated": "False", + "system_use_utf8": "False", + "visibility": "hidden", + "with_stacktrace_backtrace": "True", + "without_atomic": "False", + "without_chrono": "False", + "without_container": "False", + "without_context": "False", + "without_contract": "False", + "without_coroutine": "False", + "without_date_time": "False", + "without_exception": "False", + "without_fiber": "False", + "without_filesystem": "False", + "without_graph": "False", + "without_graph_parallel": "True", + "without_iostreams": "False", + "without_json": "False", + "without_locale": "False", + "without_log": "False", + "without_math": "False", + "without_mpi": "True", + "without_nowide": "False", + "without_program_options": "False", + "without_python": "True", + "without_random": "False", + "without_regex": "False", + "without_serialization": "False", + "without_stacktrace": "False", + "without_system": "False", + "without_test": "False", + "without_thread": "False", + "without_timer": "False", + "without_type_erasure": "False", + "without_wave": "False", + "zlib": "True", + "zstd": "False", + "bzip2:build_executable": "True", + "bzip2:fPIC": "True", + "bzip2:shared": "False", + "libbacktrace:fPIC": "True", + "libbacktrace:shared": "False", + "zlib:fPIC": "True", + "zlib:shared": "False", + }, + Context: "host", + PackageID: "dc8aedd23a0f0a773a5fcdcfe1ae3e89c4205978", + Prev: "b9d7912e6131dfa453c725593b36c808", + }, + }, { Name: "zlib", Version: "1.2.12", @@ -21,19 +200,114 @@ func TestParseConanlock(t *testing.T) { Type: pkg.ConanPkg, MetadataType: pkg.ConanLockMetadataType, Metadata: pkg.ConanLockMetadata{ - Ref: "zlib/1.2.12", + Ref: "zlib/1.2.12#c67ce17f2e96b972d42393ce50a76a1a", + Options: map[string]string{ + "fPIC": "True", + "shared": "False", + }, + Context: "host", + PackageID: "dfbe50feef7f3c6223a476cd5aeadb687084a646", + Prev: "7cd359d44f89ab08e33b5db75605002c", + }, + }, + { + Name: "bzip2", + Version: "1.0.8", + PURL: "pkg:conan/bzip2@1.0.8", + Locations: file.NewLocationSet(file.NewLocation(fixture)), + Language: pkg.CPP, + Type: pkg.ConanPkg, + MetadataType: pkg.ConanLockMetadataType, + Metadata: pkg.ConanLockMetadata{ + Ref: "bzip2/1.0.8#62a8031289639043797cf53fa876d0ef", + Options: map[string]string{ + "build_executable": "True", + "fPIC": "True", + "shared": "False", + }, + Context: "host", + PackageID: "c32092bf4d4bb47cf962af898e02823f499b017e", + Prev: "b746948bc999d6f17f52a1f76e729e80", + }, + }, + { + Name: "libbacktrace", + Version: "cci.20210118", + PURL: "pkg:conan/libbacktrace@cci.20210118", + Locations: file.NewLocationSet(file.NewLocation(fixture)), + Language: pkg.CPP, + Type: pkg.ConanPkg, + MetadataType: pkg.ConanLockMetadataType, + Metadata: pkg.ConanLockMetadata{ + Ref: "libbacktrace/cci.20210118#76e40b760e0bcd602d46db56b22820ab", + Options: map[string]string{ + "fPIC": "True", + "shared": "False", + }, + Context: "host", + PackageID: "dfbe50feef7f3c6223a476cd5aeadb687084a646", + Prev: "98a976f017e894c27e9a158b807ec0c7", + }, + }, + { + Name: "tinyxml2", + Version: "9.0.0", + PURL: "pkg:conan/tinyxml2@9.0.0", + Locations: file.NewLocationSet(file.NewLocation(fixture)), + Language: pkg.CPP, + Type: pkg.ConanPkg, + MetadataType: pkg.ConanLockMetadataType, + Metadata: pkg.ConanLockMetadata{ + Ref: "tinyxml2/9.0.0#9f13a36ebfc222cd55fe531a0a8d94d1", Options: map[string]string{ "fPIC": "True", "shared": "False", }, - Path: "all/conanfile.py", Context: "host", + // intentionally remove to test missing PackageID and Prev + // PackageID: "6557f18ca99c0b6a233f43db00e30efaa525e27e", + // Prev: "548bb273d2980991baa519453d68e5cd", }, }, } - // TODO: relationships are not under test - var expectedRelationships []artifact.Relationship + // relationships require IDs to be set to be sorted similarly + for i := range expected { + expected[i].SetID() + } + + var expectedRelationships = []artifact.Relationship{ + { + From: expected[1], // boost + To: expected[0], // mfast + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: expected[5], // tinyxml2 + To: expected[0], // mfast + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: expected[2], // zlib + To: expected[1], // boost + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: expected[3], // bzip2 + To: expected[1], // boost + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + { + From: expected[4], // libbacktrace + To: expected[1], // boost + Type: artifact.DependencyOfRelationship, + Data: nil, + }, + } pkgtest.TestFileParser(t, fixture, parseConanlock, expected, expectedRelationships) } diff --git a/syft/pkg/cataloger/cpp/test-fixtures/README.md b/syft/pkg/cataloger/cpp/test-fixtures/README.md new file mode 100644 index 000000000..3b2703366 --- /dev/null +++ b/syft/pkg/cataloger/cpp/test-fixtures/README.md @@ -0,0 +1,17 @@ +# Conan test data + +This folder contains the test data for the Conan package manager. + +## conan.lock + +The conan lock file is created in the following way. + +We explicitly use a package which has dependencies, which in turn also have dependendencies. +This is necessary to verify that the dependency tree is properly parsed. + +1. Use `conan lock create --reference "mfast/1.2.2#c6f6387c9b99780f0ee05e25f99d0f39"` +2. Manually modify the user and channel of mfast package, to be able to test that it is properly set in SBOM: + `sed -i 's|mfast/1.2.2#c6f6387c9b99780f0ee05e25f99d0f39|mfast/1.2.2@my_user/my_channel#c6f6387c9b99780f0ee05e25f99d0f39|g' conan.lock` +3. Manually delete the package id and prev from tinyxml2 entry to test conan lock parsing if they are missing: + `sed -i 's|\"package_id\": \"6557f18ca99c0b6a233f43db00e30efaa525e27e\",||g' conan.lock` + `sed -i 's|\"prev\": \"548bb273d2980991baa519453d68e5cd\",||g' conan.lock` \ No newline at end of file diff --git a/syft/pkg/cataloger/cpp/test-fixtures/conan.lock b/syft/pkg/cataloger/cpp/test-fixtures/conan.lock index cbae840d9..3afb74890 100644 --- a/syft/pkg/cataloger/cpp/test-fixtures/conan.lock +++ b/syft/pkg/cataloger/cpp/test-fixtures/conan.lock @@ -1,16 +1,60 @@ { "graph_lock": { "nodes": { - "0": { - "ref": "zlib/1.2.12", + "1": { + "ref": "mfast/1.2.2@my_user/my_channel#c6f6387c9b99780f0ee05e25f99d0f39", + "options": "fPIC=True\nshared=False\nwith_sqlite3=False\nboost:addr2line_location=/usr/bin/addr2line\nboost:asio_no_deprecated=False\nboost:buildid=None\nboost:bzip2=True\nboost:debug_level=0\nboost:diagnostic_definitions=False\nboost:error_code_header_only=False\nboost:extra_b2_flags=None\nboost:fPIC=True\nboost:filesystem_no_deprecated=False\nboost:header_only=False\nboost:i18n_backend=deprecated\nboost:i18n_backend_iconv=libc\nboost:i18n_backend_icu=False\nboost:layout=system\nboost:lzma=False\nboost:magic_autolink=False\nboost:multithreading=True\nboost:namespace=boost\nboost:namespace_alias=False\nboost:numa=True\nboost:pch=True\nboost:python_executable=None\nboost:python_version=None\nboost:segmented_stacks=False\nboost:shared=False\nboost:system_no_deprecated=False\nboost:system_use_utf8=False\nboost:visibility=hidden\nboost:with_stacktrace_backtrace=True\nboost:without_atomic=False\nboost:without_chrono=False\nboost:without_container=False\nboost:without_context=False\nboost:without_contract=False\nboost:without_coroutine=False\nboost:without_date_time=False\nboost:without_exception=False\nboost:without_fiber=False\nboost:without_filesystem=False\nboost:without_graph=False\nboost:without_graph_parallel=True\nboost:without_iostreams=False\nboost:without_json=False\nboost:without_locale=False\nboost:without_log=False\nboost:without_math=False\nboost:without_mpi=True\nboost:without_nowide=False\nboost:without_program_options=False\nboost:without_python=True\nboost:without_random=False\nboost:without_regex=False\nboost:without_serialization=False\nboost:without_stacktrace=False\nboost:without_system=False\nboost:without_test=False\nboost:without_thread=False\nboost:without_timer=False\nboost:without_type_erasure=False\nboost:without_wave=False\nboost:zlib=True\nboost:zstd=False\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nlibbacktrace:fPIC=True\nlibbacktrace:shared=False\ntinyxml2:fPIC=True\ntinyxml2:shared=False\nzlib:fPIC=True\nzlib:shared=False", + "package_id": "9d1f076b471417647c2022a78d5e2c1f834289ac", + "prev": "0ca9799450422cc55a92ccc6ffd57fba", + "requires": [ + "2", + "6" + ], + "context": "host" + }, + "2": { + "ref": "boost/1.75.0#a9c318f067216f900900e044e7af4ab1", + "options": "addr2line_location=/usr/bin/addr2line\nasio_no_deprecated=False\nbuildid=None\nbzip2=True\ndebug_level=0\ndiagnostic_definitions=False\nerror_code_header_only=False\nextra_b2_flags=None\nfPIC=True\nfilesystem_no_deprecated=False\nheader_only=False\ni18n_backend=deprecated\ni18n_backend_iconv=libc\ni18n_backend_icu=False\nlayout=system\nlzma=False\nmagic_autolink=False\nmultithreading=True\nnamespace=boost\nnamespace_alias=False\nnuma=True\npch=True\npython_executable=None\npython_version=None\nsegmented_stacks=False\nshared=False\nsystem_no_deprecated=False\nsystem_use_utf8=False\nvisibility=hidden\nwith_stacktrace_backtrace=True\nwithout_atomic=False\nwithout_chrono=False\nwithout_container=False\nwithout_context=False\nwithout_contract=False\nwithout_coroutine=False\nwithout_date_time=False\nwithout_exception=False\nwithout_fiber=False\nwithout_filesystem=False\nwithout_graph=False\nwithout_graph_parallel=True\nwithout_iostreams=False\nwithout_json=False\nwithout_locale=False\nwithout_log=False\nwithout_math=False\nwithout_mpi=True\nwithout_nowide=False\nwithout_program_options=False\nwithout_python=True\nwithout_random=False\nwithout_regex=False\nwithout_serialization=False\nwithout_stacktrace=False\nwithout_system=False\nwithout_test=False\nwithout_thread=False\nwithout_timer=False\nwithout_type_erasure=False\nwithout_wave=False\nzlib=True\nzstd=False\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nlibbacktrace:fPIC=True\nlibbacktrace:shared=False\nzlib:fPIC=True\nzlib:shared=False", + "package_id": "dc8aedd23a0f0a773a5fcdcfe1ae3e89c4205978", + "prev": "b9d7912e6131dfa453c725593b36c808", + "requires": [ + "3", + "4", + "5" + ], + "context": "host" + }, + "3": { + "ref": "zlib/1.2.12#c67ce17f2e96b972d42393ce50a76a1a", "options": "fPIC=True\nshared=False", - "requires": [], - "path": "all/conanfile.py", + "package_id": "dfbe50feef7f3c6223a476cd5aeadb687084a646", + "prev": "7cd359d44f89ab08e33b5db75605002c", + "context": "host" + }, + "4": { + "ref": "bzip2/1.0.8#62a8031289639043797cf53fa876d0ef", + "options": "build_executable=True\nfPIC=True\nshared=False", + "package_id": "c32092bf4d4bb47cf962af898e02823f499b017e", + "prev": "b746948bc999d6f17f52a1f76e729e80", + "context": "host" + }, + "5": { + "ref": "libbacktrace/cci.20210118#76e40b760e0bcd602d46db56b22820ab", + "options": "fPIC=True\nshared=False", + "package_id": "dfbe50feef7f3c6223a476cd5aeadb687084a646", + "prev": "98a976f017e894c27e9a158b807ec0c7", + "context": "host" + }, + "6": { + "ref": "tinyxml2/9.0.0#9f13a36ebfc222cd55fe531a0a8d94d1", + "options": "fPIC=True\nshared=False", + + "context": "host" } }, - "revisions_enabled": false + "revisions_enabled": true }, "version": "0.4", - "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=9\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n" -} + "profile_host": "[settings]\narch=x86_64\narch.march=None\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++11\ncompiler.version=11\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n" +} \ No newline at end of file diff --git a/syft/pkg/cataloger/cpp/test-fixtures/conanfile.txt b/syft/pkg/cataloger/cpp/test-fixtures/conanfile.txt index c265b1328..21db293a9 100644 --- a/syft/pkg/cataloger/cpp/test-fixtures/conanfile.txt +++ b/syft/pkg/cataloger/cpp/test-fixtures/conanfile.txt @@ -4,9 +4,9 @@ catch2/2.13.8 docopt.cpp/0.6.3 fmt/8.1.1 -spdlog/1.9.2 -sdl/2.0.20 -fltk/1.3.8 +spdlog/1.9.2@my_user/my_channel#1234567%%987654 +sdl/2.0.20#1234567%%987654 +fltk/1.3.8@my_user/my_channel [generators] cmake_find_package_multi diff --git a/syft/pkg/cataloger/deb/cataloger_test.go b/syft/pkg/cataloger/deb/cataloger_test.go index 6e035be87..e547206e5 100644 --- a/syft/pkg/cataloger/deb/cataloger_test.go +++ b/syft/pkg/cataloger/deb/cataloger_test.go @@ -44,6 +44,11 @@ func TestDpkgCataloger(t *testing.T) { Contains configuration files and directories required for authentication to work on Debian systems. This package is required on almost all installations.`, + Depends: []string{ + "debconf (>= 0.5) | debconf-2.0", + "debconf (>= 1.5.19) | cdebconf", + "libpam-modules (>= 1.0.1-6)", + }, Files: []pkg.DpkgFileRecord{ { Path: "/etc/pam.conf", @@ -112,6 +117,7 @@ func TestDpkgCataloger(t *testing.T) { SQLite is a C library that implements an SQL database engine. Programs that link with the SQLite library can have SQL database access without running a separate RDBMS process.`, + Depends: []string{"libc6 (>= 2.29)"}, Files: []pkg.DpkgFileRecord{ {Path: "/usr/lib/aarch64-linux-gnu/libsqlite3.so.0.8.6", Digest: &file.Digest{ Algorithm: "md5", diff --git a/syft/pkg/cataloger/deb/package.go b/syft/pkg/cataloger/deb/package.go index ebd72a77a..eb9b551c9 100644 --- a/syft/pkg/cataloger/deb/package.go +++ b/syft/pkg/cataloger/deb/package.go @@ -36,13 +36,15 @@ func newDpkgPackage(d pkg.DpkgMetadata, dbLocation file.Location, resolver file. Metadata: d, } - // the current entry only has what may have been listed in the status file, however, there are additional - // files that are listed in multiple other locations. We should retrieve them all and merge the file lists - // together. - mergeFileListing(resolver, dbLocation, &p) + if resolver != nil { + // the current entry only has what may have been listed in the status file, however, there are additional + // files that are listed in multiple other locations. We should retrieve them all and merge the file lists + // together. + mergeFileListing(resolver, dbLocation, &p) - // fetch additional data from the copyright file to derive the license information - addLicenses(resolver, dbLocation, &p) + // fetch additional data from the copyright file to derive the license information + addLicenses(resolver, dbLocation, &p) + } p.SetID() diff --git a/syft/pkg/cataloger/deb/parse_dpkg_db.go b/syft/pkg/cataloger/deb/parse_dpkg_db.go index 0a7dccb2d..03c66c583 100644 --- a/syft/pkg/cataloger/deb/parse_dpkg_db.go +++ b/syft/pkg/cataloger/deb/parse_dpkg_db.go @@ -35,7 +35,7 @@ func parseDpkgDB(resolver file.Resolver, env *generic.Environment, reader file.L pkgs = append(pkgs, newDpkgPackage(m, reader.Location, resolver, env.LinuxRelease)) } - return pkgs, nil, nil + return pkgs, associateRelationships(pkgs), nil } // parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed. @@ -63,6 +63,22 @@ func parseDpkgStatus(reader io.Reader) ([]pkg.DpkgMetadata, error) { return metadata, nil } +// dpkgExtractedMetadata is an adapter struct to capture the fields from the dpkg status file, however, the final +// pkg.DpkgMetadata struct has different types for some fields (e.g. Provides, Depends, and PreDepends is []string, not a string). +type dpkgExtractedMetadata struct { + Package string `mapstructure:"Package"` + Source string `mapstructure:"Source"` + Version string `mapstructure:"Version"` + SourceVersion string `mapstructure:"SourceVersion"` + Architecture string `mapstructure:"Architecture"` + Maintainer string `mapstructure:"Maintainer"` + InstalledSize int `mapstructure:"InstalledSize"` + Description string `mapstructure:"Description"` + Provides string `mapstructure:"Provides"` + Depends string `mapstructure:"Depends"` + PreDepends string `mapstructure:"PreDepends"` // note: original doc is Pre-Depends +} + // parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader. func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgMetadata, error) { var retErr error @@ -77,22 +93,36 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgMetadata, error) { retErr = err } - entry := pkg.DpkgMetadata{} - err = mapstructure.Decode(dpkgFields, &entry) + raw := dpkgExtractedMetadata{} + err = mapstructure.Decode(dpkgFields, &raw) if err != nil { return nil, err } - sourceName, sourceVersion := extractSourceVersion(entry.Source) + sourceName, sourceVersion := extractSourceVersion(raw.Source) if sourceVersion != "" { - entry.SourceVersion = sourceVersion - entry.Source = sourceName + raw.SourceVersion = sourceVersion + raw.Source = sourceName } - if entry.Package == "" { + if raw.Package == "" { return nil, retErr } + entry := pkg.DpkgMetadata{ + Package: raw.Package, + Source: raw.Source, + Version: raw.Version, + SourceVersion: raw.SourceVersion, + Architecture: raw.Architecture, + Maintainer: raw.Maintainer, + InstalledSize: raw.InstalledSize, + Description: raw.Description, + Provides: splitPkgList(raw.Provides), + Depends: splitPkgList(raw.Depends), + PreDepends: splitPkgList(raw.PreDepends), + } + // there may be an optional conffiles section that we should persist as files if conffilesSection, exists := dpkgFields["Conffiles"]; exists && conffilesSection != nil { if sectionStr, ok := conffilesSection.(string); ok { @@ -108,6 +138,17 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgMetadata, error) { return &entry, retErr } +func splitPkgList(pkgList string) (ret []string) { + fields := strings.Split(pkgList, ",") + for _, field := range fields { + field = strings.TrimSpace(field) + if field != "" { + ret = append(ret, field) + } + } + return ret +} + func extractAllFields(reader *bufio.Reader) (map[string]interface{}, error) { dpkgFields := make(map[string]interface{}) var key string @@ -195,3 +236,79 @@ func handleNewKeyValue(line string) (key string, val interface{}, err error) { return "", nil, fmt.Errorf("cannot parse field from line: '%s'", line) } + +// associateRelationships will create relationships between packages based on the "Depends", "Pre-Depends", and "Provides" +// fields for installed packages. if there is an installed package that has a dependency that is (somehow) not installed, +// then that relationship (between the installed and uninstalled package) will NOT be created. +func associateRelationships(pkgs []pkg.Package) (relationships []artifact.Relationship) { + // map["provides" + "package"] -> packages that provide that package + lookup := make(map[string][]pkg.Package) + + // read provided and add as keys for lookup keys as well as package names + for _, p := range pkgs { + meta, ok := p.Metadata.(pkg.DpkgMetadata) + if !ok { + log.Warnf("cataloger failed to extract dpkg 'provides' metadata for package %+v", p.Name) + continue + } + lookup[p.Name] = append(lookup[p.Name], p) + for _, provides := range meta.Provides { + k := stripVersionSpecifier(provides) + lookup[k] = append(lookup[k], p) + } + } + + // read "Depends" and "Pre-Depends" and match with keys + for _, p := range pkgs { + meta, ok := p.Metadata.(pkg.DpkgMetadata) + if !ok { + log.Warnf("cataloger failed to extract dpkg 'dependency' metadata for package %+v", p.Name) + continue + } + + var allDeps []string + allDeps = append(allDeps, meta.Depends...) + allDeps = append(allDeps, meta.PreDepends...) + + for _, depSpecifier := range allDeps { + deps := splitPackageChoice(depSpecifier) + for _, dep := range deps { + for _, depPkg := range lookup[dep] { + relationships = append(relationships, artifact.Relationship{ + From: depPkg, + To: p, + Type: artifact.DependencyOfRelationship, + }) + } + } + } + } + return relationships +} + +func stripVersionSpecifier(s string) string { + // examples: + // libgmp10 (>= 2:6.2.1+dfsg1) --> libgmp10 + // libgmp10 --> libgmp10 + // foo [i386] --> foo + // default-mta | mail-transport-agent --> default-mta | mail-transport-agent + // kernel-headers-2.2.10 [!hurd-i386] --> kernel-headers-2.2.10 + + items := internal.SplitAny(s, "[(<>=") + if len(items) == 0 { + return s + } + + return strings.TrimSpace(items[0]) +} + +func splitPackageChoice(s string) (ret []string) { + fields := strings.Split(s, "|") + for _, field := range fields { + field = strings.TrimSpace(field) + if field != "" { + ret = append(ret, stripVersionSpecifier(field)) + } + } + return ret +} diff --git a/syft/pkg/cataloger/deb/parse_dpkg_db_test.go b/syft/pkg/cataloger/deb/parse_dpkg_db_test.go index 0a2c58bd8..b2d72f287 100644 --- a/syft/pkg/cataloger/deb/parse_dpkg_db_test.go +++ b/syft/pkg/cataloger/deb/parse_dpkg_db_test.go @@ -11,9 +11,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) @@ -48,6 +50,18 @@ func Test_parseDpkgStatus(t *testing.T) { * apt-cdrom to use removable media as a source for packages * apt-config as an interface to the configuration settings * apt-key as an interface to manage authentication keys`, + Provides: []string{"apt-transport-https (= 1.8.2)"}, + Depends: []string{ + "adduser", + "gpgv | gpgv2 | gpgv1", + "debian-archive-keyring", + "libapt-pkg5.0 (>= 1.7.0~alpha3~)", + "libc6 (>= 2.15)", + "libgcc1 (>= 1:3.0)", + "libgnutls30 (>= 3.6.6)", + "libseccomp2 (>= 1.0.1)", + "libstdc++6 (>= 5.2)", + }, Files: []pkg.DpkgFileRecord{ { Path: "/etc/apt/apt.conf.d/01autoremove", @@ -110,6 +124,18 @@ func Test_parseDpkgStatus(t *testing.T) { * apt-cdrom to use removable media as a source for packages * apt-config as an interface to the configuration settings * apt-key as an interface to manage authentication keys`, + Provides: []string{"apt-transport-https (= 1.8.2)"}, + Depends: []string{ + "adduser", + "gpgv | gpgv2 | gpgv1", + "debian-archive-keyring", + "libapt-pkg5.0 (>= 1.7.0~alpha3~)", + "libc6 (>= 2.15)", + "libgcc1 (>= 1:3.0)", + "libgnutls30 (>= 3.6.6)", + "libseccomp2 (>= 1.0.1)", + "libstdc++6 (>= 5.2)", + }, Files: []pkg.DpkgFileRecord{}, }, }, @@ -135,7 +161,9 @@ func Test_parseDpkgStatus(t *testing.T) { globe. It is updated periodically to reflect changes made by political bodies to time zone boundaries, UTC offsets, and daylight-saving rules.`, - Files: []pkg.DpkgFileRecord{}, + Provides: []string{"tzdata-buster"}, + Depends: []string{"debconf (>= 0.5) | debconf-2.0"}, + Files: []pkg.DpkgFileRecord{}, }, { Package: "util-linux", @@ -149,6 +177,14 @@ func Test_parseDpkgStatus(t *testing.T) { important utilities included in this package allow you to view kernel messages, create new filesystems, view block device information, interface with real time clock, etc.`, + Depends: []string{"fdisk", "login (>= 1:4.5-1.1~)"}, + PreDepends: []string{ + "libaudit1 (>= 1:2.2.1)", "libblkid1 (>= 2.31.1)", "libc6 (>= 2.25)", + "libcap-ng0 (>= 0.7.9)", "libmount1 (>= 2.25)", "libpam0g (>= 0.99.7.1)", + "libselinux1 (>= 2.6-3~)", "libsmartcols1 (>= 2.33)", "libsystemd0", + "libtinfo6 (>= 6)", "libudev1 (>= 183)", "libuuid1 (>= 2.16)", + "zlib1g (>= 1:1.1.4)", + }, Files: []pkg.DpkgFileRecord{ { Path: "/etc/default/hwclock", @@ -397,3 +433,122 @@ func Test_handleNewKeyValue(t *testing.T) { }) } } + +func Test_stripVersionSpecifier(t *testing.T) { + + tests := []struct { + name string + input string + want string + }{ + { + name: "package name only", + input: "test", + want: "test", + }, + { + name: "with version", + input: "test (1.2.3)", + want: "test", + }, + { + name: "multiple packages", + input: "test | other", + want: "test | other", + }, + { + name: "with architecture specifiers", + input: "test [amd64 i386]", + want: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, stripVersionSpecifier(tt.input)) + }) + } +} + +func Test_associateRelationships(t *testing.T) { + tests := []struct { + name string + fixture string + wantRelationships map[string][]string + }{ + { + name: "relationships for coreutils", + fixture: "test-fixtures/status/coreutils-relationships", + wantRelationships: map[string][]string{ + "coreutils": {"libacl1", "libattr1", "libc6", "libgmp10", "libselinux1"}, + "libacl1": {"libc6"}, + "libattr1": {"libc6"}, + "libc6": {"libgcc-s1"}, + "libgcc-s1": {"gcc-12-base", "libc6"}, + "libgmp10": {"libc6"}, + "libpcre2-8-0": {"libc6"}, + "libselinux1": {"libc6", "libpcre2-8-0"}, + }, + }, + { + name: "relationships from dpkg example docs", + fixture: "test-fixtures/status/doc-examples", + wantRelationships: map[string][]string{ + "made-up-package-1": {"kernel-headers-2.2.10", "hurd-dev", "gnumach-dev"}, + "made-up-package-2": {"libluajit5.1-dev", "liblua5.1-dev"}, + "made-up-package-3": {"foo", "bar"}, + // note that the "made-up-package-4" depends on "made-up-package-5" but not via the direct + // package name, but through the "provides" virtual package name "virtual-package-5". + "made-up-package-4": {"made-up-package-5"}, + // note that though there is a "default-mta | mail-transport-agent | not-installed" + // dependency choice we raise up the packages that are installed for every choice. + // In this case that means that "default-mta" and "mail-transport-agent". + "mutt": {"libc6", "default-mta", "mail-transport-agent"}, + }, + }, + { + name: "relationships for libpam-runtime", + fixture: "test-fixtures/status/libpam-runtime", + wantRelationships: map[string][]string{ + "libpam-runtime": {"debconf1", "debconf-2.0", "debconf2", "cdebconf", "libpam-modules"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.fixture) + require.NoError(t, err) + + reader := file.NewLocationReadCloser(file.NewLocation(tt.fixture), f) + + pkgs, relationships, err := parseDpkgDB(nil, &generic.Environment{}, reader) + require.NotEmpty(t, pkgs) + require.NotEmpty(t, relationships) + require.NoError(t, err) + + if d := cmp.Diff(tt.wantRelationships, abstractRelationships(t, relationships)); d != "" { + t.Errorf("unexpected relationships (-want +got):\n%s", d) + } + }) + } +} + +func abstractRelationships(t testing.TB, relationships []artifact.Relationship) map[string][]string { + t.Helper() + + abstracted := make(map[string][]string) + for _, relationship := range relationships { + fromPkg, ok := relationship.From.(pkg.Package) + if !ok { + continue + } + toPkg, ok := relationship.To.(pkg.Package) + if !ok { + continue + } + + // we build this backwards since we use DependencyOfRelationship instead of DependsOn + abstracted[toPkg.Name] = append(abstracted[toPkg.Name], fromPkg.Name) + } + + return abstracted +} diff --git a/syft/pkg/cataloger/deb/test-fixtures/status/coreutils-relationships b/syft/pkg/cataloger/deb/test-fixtures/status/coreutils-relationships new file mode 100644 index 000000000..1097fea56 --- /dev/null +++ b/syft/pkg/cataloger/deb/test-fixtures/status/coreutils-relationships @@ -0,0 +1,114 @@ +Package: coreutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 20272 +Maintainer: Michael Stone +Architecture: arm64 +Multi-Arch: foreign +Version: 9.1-1 +Pre-Depends: libacl1 (>= 2.2.23), libattr1 (>= 1:2.4.44), libc6 (>= 2.34), libgmp10 (>= 2:6.2.1+dfsg1), libselinux1 (>= 3.1~) + +Package: libacl1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 101 +Maintainer: Guillem Jover +Architecture: arm64 +Multi-Arch: same +Source: acl +Version: 2.3.1-3 +Depends: libc6 (>= 2.33) + +Package: libc6 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 23127 +Maintainer: GNU Libc Maintainers +Architecture: arm64 +Multi-Arch: same +Source: glibc +Version: 2.36-9+deb12u1 +Depends: libgcc-s1 +Recommends: libidn2-0 (>= 2.0.5~) +Suggests: glibc-doc, debconf | debconf-2.0, libc-l10n, locales, libnss-nis, libnss-nisplus +Breaks: aide (<< 0.17.3-4+b3), busybox (<< 1.30.1-6), chrony (<< 4.2-3~), fakechroot (<< 2.19-3.5), firefox (<< 91~), firefox-esr (<< 91~), gnumach-image-1.8-486 (<< 2:1.8+git20210923~), gnumach-image-1.8-486-dbg (<< 2:1.8+git20210923~), gnumach-image-1.8-xen-486 (<< 2:1.8+git20210923~), gnumach-image-1.8-xen-486-dbg (<< 2:1.8+git20210923~), hurd (<< 1:0.9.git20220301-2), ioquake3 (<< 1.36+u20200211.f2c61c1~dfsg-2~), iraf-fitsutil (<< 2018.07.06-4), libgegl-0.4-0 (<< 0.4.18), libtirpc1 (<< 0.2.3), locales (<< 2.36), locales-all (<< 2.36), macs (<< 2.2.7.1-3~), nocache (<< 1.1-1~), nscd (<< 2.36), openarena (<< 0.8.8+dfsg-4~), openssh-server (<< 1:8.1p1-5), python3-iptables (<< 1.0.0-2), r-cran-later (<< 0.7.5+dfsg-2), tinydns (<< 1:1.05-14), valgrind (<< 1:3.19.0-1~), wcc (<< 0.0.2+dfsg-3) + +Package: libgcc-s1 +Protected: yes +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 147 +Maintainer: Debian GCC Maintainers +Architecture: arm64 +Multi-Arch: same +Source: gcc-12 +Version: 12.2.0-14 +Replaces: libgcc1 (<< 1:10) +Provides: libgcc1 (= 1:12.2.0-14) +Depends: gcc-12-base (= 12.2.0-14), libc6 (>= 2.35) + +Package: gcc-12-base +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 100 +Maintainer: Debian GCC Maintainers +Architecture: arm64 +Multi-Arch: same +Source: gcc-12 +Version: 12.2.0-14 +Breaks: gnat (<< 7) + +Package: libattr1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 99 +Maintainer: Guillem Jover +Architecture: arm64 +Multi-Arch: same +Source: attr +Version: 1:2.5.1-4 +Depends: libc6 (>= 2.17) + +Package: libgmp10 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 855 +Maintainer: Debian Science Team +Architecture: arm64 +Multi-Arch: same +Source: gmp +Version: 2:6.2.1+dfsg1-1.1 +Depends: libc6 (>= 2.17) +Breaks: libmath-gmp-perl (<< 2.20-1), libmath-prime-util-gmp-perl (<< 0.51-2), postgresql-pgmp (<< 1.0.3-1) + +Package: libselinux1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 223 +Maintainer: Debian SELinux maintainers +Architecture: arm64 +Multi-Arch: same +Source: libselinux (3.4-1) +Version: 3.4-1+b6 +Depends: libc6 (>= 2.34), libpcre2-8-0 (>= 10.22) + +Package: libpcre2-8-0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 649 +Maintainer: Matthew Vernon +Architecture: arm64 +Multi-Arch: same +Source: pcre2 +Version: 10.42-1 +Depends: libc6 (>= 2.34) diff --git a/syft/pkg/cataloger/deb/test-fixtures/status/doc-examples b/syft/pkg/cataloger/deb/test-fixtures/status/doc-examples new file mode 100644 index 000000000..6b9fe186f --- /dev/null +++ b/syft/pkg/cataloger/deb/test-fixtures/status/doc-examples @@ -0,0 +1,46 @@ +Package: mutt +Version: 1.3.17-1 +Depends: libc6 (>= 2.2.1), default-mta | mail-transport-agent | not-installed + +Package: made-up-package-1 +Version: 1.0.0 +Source: glibc +Depends: kernel-headers-2.2.10 [!hurd-i386], + hurd-dev [hurd-i386], gnumach-dev [hurd-i386] + +Package: made-up-package-2 +Version: 2.0.0 +Depends: + libluajit5.1-dev [i386 amd64 kfreebsd-i386 armel armhf powerpc mips], + liblua5.1-dev [hurd-i386 ia64 kfreebsd-amd64 s390x sparc], + +Package: made-up-package-3 +Version: 3.0.0 +Depends: foo [i386], bar [amd64] + +Package: made-up-package-4 +Version: 3.0.0 +Depends: virtual-package-5 + +Package: made-up-package-5 +Provides: virtual-package-5 (=1.0) + +Package: foo + +Package: bar + +Package: kernel-headers-2.2.10 + +Package: hurd-dev + +Package: gnumach-dev + +Package: default-mta + +Package: mail-transport-agent + +Package: libc6 + +Package: libluajit5.1-dev + +Package: liblua5.1-dev diff --git a/syft/pkg/cataloger/deb/test-fixtures/status/libpam-runtime b/syft/pkg/cataloger/deb/test-fixtures/status/libpam-runtime new file mode 100644 index 000000000..73aaf6978 --- /dev/null +++ b/syft/pkg/cataloger/deb/test-fixtures/status/libpam-runtime @@ -0,0 +1,23 @@ +Package: libpam-runtime +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 876 +Maintainer: Sam Hartman +Architecture: all +Multi-Arch: foreign +Source: pam +Version: 1.5.2-6+deb12u1 +Replaces: libpam0g-dev, libpam0g-util +Depends: debconf1 (>= 0.5) | debconf-2.0, debconf2 (>= 1.5.19) | cdebconf, libpam-modules (>= 1.0.1-6) +Conflicts: libpam0g-util + +Package: debconf1 + +Package: debconf2 + +Package: debconf-2.0 + +Package: cdebconf + +Package: libpam-modules diff --git a/syft/pkg/cataloger/dotnet/package.go b/syft/pkg/cataloger/dotnet/package.go index c8cb261a6..a7b3b209f 100644 --- a/syft/pkg/cataloger/dotnet/package.go +++ b/syft/pkg/cataloger/dotnet/package.go @@ -1,6 +1,8 @@ package dotnet import ( + "fmt" + "regexp" "strings" "github.com/anchore/packageurl-go" @@ -9,13 +11,7 @@ import ( ) func newDotnetDepsPackage(nameVersion string, lib dotnetDepsLibrary, locations ...file.Location) *pkg.Package { - if lib.Type != "package" { - return nil - } - - fields := strings.Split(nameVersion, "/") - name := fields[0] - version := fields[1] + name, version := extractNameAndVersion(nameVersion) m := pkg.DotnetDepsMetadata{ Name: name, @@ -41,6 +37,27 @@ func newDotnetDepsPackage(nameVersion string, lib dotnetDepsLibrary, locations . return p } +func getDepsJSONFilePrefix(p string) string { + r := regexp.MustCompile(`([^\/]+)\.deps\.json$`) + match := r.FindStringSubmatch(p) + if len(match) > 1 { + return match[1] + } + return "" +} + +func extractNameAndVersion(nameVersion string) (name, version string) { + fields := strings.Split(nameVersion, "/") + name = fields[0] + version = fields[1] + return +} + +func createNameAndVersion(name, version string) (nameVersion string) { + nameVersion = fmt.Sprintf("%s/%s", name, version) + return +} + func packageURL(m pkg.DotnetDepsMetadata) string { var qualifiers packageurl.Qualifiers diff --git a/syft/pkg/cataloger/dotnet/parse_dotnet_deps.go b/syft/pkg/cataloger/dotnet/parse_dotnet_deps.go index 2c7e1cf0b..e99811ac8 100644 --- a/syft/pkg/cataloger/dotnet/parse_dotnet_deps.go +++ b/syft/pkg/cataloger/dotnet/parse_dotnet_deps.go @@ -5,6 +5,7 @@ import ( "fmt" "sort" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" @@ -13,8 +14,18 @@ import ( var _ generic.Parser = parseDotnetDeps +type dotnetRuntimeTarget struct { + Name string `json:"name"` +} + +type dotnetDepsTarget struct { + Dependencies map[string]string `json:"dependencies"` + Runtime map[string]struct{} `json:"runtime"` +} type dotnetDeps struct { - Libraries map[string]dotnetDepsLibrary `json:"libraries"` + RuntimeTarget dotnetRuntimeTarget `json:"runtimeTarget"` + Targets map[string]map[string]dotnetDepsTarget `json:"targets"` + Libraries map[string]dotnetDepsLibrary `json:"libraries"` } type dotnetDepsLibrary struct { @@ -24,27 +35,55 @@ type dotnetDepsLibrary struct { HashPath string `json:"hashPath"` } +//nolint:funlen func parseDotnetDeps(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package + var pkgMap = make(map[string]pkg.Package) + var relationships []artifact.Relationship dec := json.NewDecoder(reader) - var p dotnetDeps - if err := dec.Decode(&p); err != nil { + var depsDoc dotnetDeps + if err := dec.Decode(&depsDoc); err != nil { return nil, nil, fmt.Errorf("failed to parse deps.json file: %w", err) } - var names []string + rootName := getDepsJSONFilePrefix(reader.AccessPath()) + if rootName == "" { + return nil, nil, fmt.Errorf("unable to determine root package name from deps.json file: %s", reader.AccessPath()) + } + var rootPkg *pkg.Package + for nameVersion, lib := range depsDoc.Libraries { + name, _ := extractNameAndVersion(nameVersion) + if lib.Type == "project" && name == rootName { + rootPkg = newDotnetDepsPackage( + nameVersion, + lib, + reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ) + } + } + if rootPkg == nil { + return nil, nil, fmt.Errorf("unable to determine root package from deps.json file: %s", reader.AccessPath()) + } + pkgs = append(pkgs, *rootPkg) + pkgMap[createNameAndVersion(rootPkg.Name, rootPkg.Version)] = *rootPkg - for nameVersion := range p.Libraries { + var names []string + for nameVersion := range depsDoc.Libraries { names = append(names, nameVersion) } - // sort the names so that the order of the packages is deterministic sort.Strings(names) for _, nameVersion := range names { - lib := p.Libraries[nameVersion] + // skip the root package + name, version := extractNameAndVersion(nameVersion) + if name == rootPkg.Name && version == rootPkg.Version { + continue + } + + lib := depsDoc.Libraries[nameVersion] dotnetPkg := newDotnetDepsPackage( nameVersion, lib, @@ -53,8 +92,36 @@ func parseDotnetDeps(_ file.Resolver, _ *generic.Environment, reader file.Locati if dotnetPkg != nil { pkgs = append(pkgs, *dotnetPkg) + pkgMap[nameVersion] = *dotnetPkg } } - return pkgs, nil, nil + for pkgNameVersion, target := range depsDoc.Targets[depsDoc.RuntimeTarget.Name] { + for depName, depVersion := range target.Dependencies { + depNameVersion := createNameAndVersion(depName, depVersion) + depPkg, ok := pkgMap[depNameVersion] + if !ok { + log.Debug("unable to find package in map", depNameVersion) + continue + } + p, ok := pkgMap[pkgNameVersion] + if !ok { + log.Debug("unable to find package in map", pkgNameVersion) + continue + } + rel := artifact.Relationship{ + From: depPkg, + To: p, + Type: artifact.DependencyOfRelationship, + } + relationships = append(relationships, rel) + } + } + + // sort the relationships for deterministic output + // TODO: ideally this would be replaced with artifact.SortRelationships when one exists and is type agnostic. + // this will only consider package-to-package relationships. + pkg.SortRelationships(relationships) + + return pkgs, relationships, nil } diff --git a/syft/pkg/cataloger/dotnet/parse_dotnet_deps_test.go b/syft/pkg/cataloger/dotnet/parse_dotnet_deps_test.go index b85353744..b71850100 100644 --- a/syft/pkg/cataloger/dotnet/parse_dotnet_deps_test.go +++ b/syft/pkg/cataloger/dotnet/parse_dotnet_deps_test.go @@ -12,202 +12,363 @@ import ( func TestParseDotnetDeps(t *testing.T) { fixture := "test-fixtures/TestLibrary.deps.json" fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture)) - expected := []pkg.Package{ - { - Name: "AWSSDK.Core", - Version: "3.7.10.6", - PURL: "pkg:nuget/AWSSDK.Core@3.7.10.6", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "AWSSDK.Core", - Version: "3.7.10.6", - Sha512: "sha512-kHBB+QmosVaG6DpngXQ8OlLVVNMzltNITfsRr68Z90qO7dSqJ2EHNd8dtBU1u3AQQLqqFHOY0lfmbpexeH6Pew==", - Path: "awssdk.core/3.7.10.6", - HashPath: "awssdk.core.3.7.10.6.nupkg.sha512", - }, + rootPkg := pkg.Package{ + Name: "TestLibrary", + Version: "1.0.0", + PURL: "pkg:nuget/TestLibrary@1.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "TestLibrary", + Version: "1.0.0", }, - { - Name: "Microsoft.Extensions.DependencyInjection.Abstractions", - Version: "6.0.0", - PURL: "pkg:nuget/Microsoft.Extensions.DependencyInjection.Abstractions@6.0.0", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "Microsoft.Extensions.DependencyInjection.Abstractions", - Version: "6.0.0", - Sha512: "sha512-xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==", - Path: "microsoft.extensions.dependencyinjection.abstractions/6.0.0", - HashPath: "microsoft.extensions.dependencyinjection.abstractions.6.0.0.nupkg.sha512", - }, + } + testCommon := pkg.Package{ + Name: "TestCommon", + Version: "1.0.0", + PURL: "pkg:nuget/TestCommon@1.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "TestCommon", + Version: "1.0.0", }, - { - Name: "Microsoft.Extensions.DependencyInjection", - Version: "6.0.0", - PURL: "pkg:nuget/Microsoft.Extensions.DependencyInjection@6.0.0", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "Microsoft.Extensions.DependencyInjection", - Version: "6.0.0", - Sha512: "sha512-k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", - Path: "microsoft.extensions.dependencyinjection/6.0.0", - HashPath: "microsoft.extensions.dependencyinjection.6.0.0.nupkg.sha512", - }, + } + awssdkcore := pkg.Package{ + Name: "AWSSDK.Core", + Version: "3.7.10.6", + PURL: "pkg:nuget/AWSSDK.Core@3.7.10.6", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "AWSSDK.Core", + Version: "3.7.10.6", + Sha512: "sha512-kHBB+QmosVaG6DpngXQ8OlLVVNMzltNITfsRr68Z90qO7dSqJ2EHNd8dtBU1u3AQQLqqFHOY0lfmbpexeH6Pew==", + Path: "awssdk.core/3.7.10.6", + HashPath: "awssdk.core.3.7.10.6.nupkg.sha512", }, - { - Name: "Microsoft.Extensions.Logging.Abstractions", - Version: "6.0.0", - PURL: "pkg:nuget/Microsoft.Extensions.Logging.Abstractions@6.0.0", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "Microsoft.Extensions.Logging.Abstractions", - Version: "6.0.0", - Sha512: "sha512-/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==", - Path: "microsoft.extensions.logging.abstractions/6.0.0", - HashPath: "microsoft.extensions.logging.abstractions.6.0.0.nupkg.sha512", - }, + } + msftDependencyInjectionAbstractions := pkg.Package{ + Name: "Microsoft.Extensions.DependencyInjection.Abstractions", + Version: "6.0.0", + PURL: "pkg:nuget/Microsoft.Extensions.DependencyInjection.Abstractions@6.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "Microsoft.Extensions.DependencyInjection.Abstractions", + Version: "6.0.0", + Sha512: "sha512-xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==", + Path: "microsoft.extensions.dependencyinjection.abstractions/6.0.0", + HashPath: "microsoft.extensions.dependencyinjection.abstractions.6.0.0.nupkg.sha512", }, - { - Name: "Microsoft.Extensions.Logging", - Version: "6.0.0", - PURL: "pkg:nuget/Microsoft.Extensions.Logging@6.0.0", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "Microsoft.Extensions.Logging", - Version: "6.0.0", - Sha512: "sha512-eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", - Path: "microsoft.extensions.logging/6.0.0", - HashPath: "microsoft.extensions.logging.6.0.0.nupkg.sha512", - }, + } + msftDependencyInjection := pkg.Package{ + Name: "Microsoft.Extensions.DependencyInjection", + Version: "6.0.0", + PURL: "pkg:nuget/Microsoft.Extensions.DependencyInjection@6.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "Microsoft.Extensions.DependencyInjection", + Version: "6.0.0", + Sha512: "sha512-k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", + Path: "microsoft.extensions.dependencyinjection/6.0.0", + HashPath: "microsoft.extensions.dependencyinjection.6.0.0.nupkg.sha512", }, - - { - Name: "Microsoft.Extensions.Options", - Version: "6.0.0", - PURL: "pkg:nuget/Microsoft.Extensions.Options@6.0.0", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "Microsoft.Extensions.Options", - Version: "6.0.0", - Sha512: "sha512-dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", - Path: "microsoft.extensions.options/6.0.0", - HashPath: "microsoft.extensions.options.6.0.0.nupkg.sha512", - }, + } + msftLoggingAbstractions := pkg.Package{ + Name: "Microsoft.Extensions.Logging.Abstractions", + Version: "6.0.0", + PURL: "pkg:nuget/Microsoft.Extensions.Logging.Abstractions@6.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "Microsoft.Extensions.Logging.Abstractions", + Version: "6.0.0", + Sha512: "sha512-/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==", + Path: "microsoft.extensions.logging.abstractions/6.0.0", + HashPath: "microsoft.extensions.logging.abstractions.6.0.0.nupkg.sha512", }, - { - Name: "Microsoft.Extensions.Primitives", - Version: "6.0.0", - PURL: "pkg:nuget/Microsoft.Extensions.Primitives@6.0.0", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "Microsoft.Extensions.Primitives", - Version: "6.0.0", - Sha512: "sha512-9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", - Path: "microsoft.extensions.primitives/6.0.0", - HashPath: "microsoft.extensions.primitives.6.0.0.nupkg.sha512", - }, + } + msftExtensionsLogging := pkg.Package{ + Name: "Microsoft.Extensions.Logging", + Version: "6.0.0", + PURL: "pkg:nuget/Microsoft.Extensions.Logging@6.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "Microsoft.Extensions.Logging", + Version: "6.0.0", + Sha512: "sha512-eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + Path: "microsoft.extensions.logging/6.0.0", + HashPath: "microsoft.extensions.logging.6.0.0.nupkg.sha512", }, - { - Name: "Newtonsoft.Json", - Version: "13.0.1", - PURL: "pkg:nuget/Newtonsoft.Json@13.0.1", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "Newtonsoft.Json", - Version: "13.0.1", - Sha512: "sha512-ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==", - Path: "newtonsoft.json/13.0.1", - HashPath: "newtonsoft.json.13.0.1.nupkg.sha512", - }, + } + msftExtensionsOptions := pkg.Package{ + Name: "Microsoft.Extensions.Options", + Version: "6.0.0", + PURL: "pkg:nuget/Microsoft.Extensions.Options@6.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "Microsoft.Extensions.Options", + Version: "6.0.0", + Sha512: "sha512-dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + Path: "microsoft.extensions.options/6.0.0", + HashPath: "microsoft.extensions.options.6.0.0.nupkg.sha512", }, - { - Name: "Serilog.Sinks.Console", - Version: "4.0.1", - PURL: "pkg:nuget/Serilog.Sinks.Console@4.0.1", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "Serilog.Sinks.Console", - Version: "4.0.1", - Sha512: "sha512-apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", - Path: "serilog.sinks.console/4.0.1", - HashPath: "serilog.sinks.console.4.0.1.nupkg.sha512", - }, + } + msftExtensionsPrimitives := pkg.Package{ + Name: "Microsoft.Extensions.Primitives", + Version: "6.0.0", + PURL: "pkg:nuget/Microsoft.Extensions.Primitives@6.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "Microsoft.Extensions.Primitives", + Version: "6.0.0", + Sha512: "sha512-9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", + Path: "microsoft.extensions.primitives/6.0.0", + HashPath: "microsoft.extensions.primitives.6.0.0.nupkg.sha512", }, - { - Name: "Serilog", - Version: "2.10.0", - PURL: "pkg:nuget/Serilog@2.10.0", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "Serilog", - Version: "2.10.0", - Sha512: "sha512-+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==", - Path: "serilog/2.10.0", - HashPath: "serilog.2.10.0.nupkg.sha512", - }, + } + newtonsoftJson := pkg.Package{ + Name: "Newtonsoft.Json", + Version: "13.0.1", + PURL: "pkg:nuget/Newtonsoft.Json@13.0.1", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "Newtonsoft.Json", + Version: "13.0.1", + Sha512: "sha512-ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==", + Path: "newtonsoft.json/13.0.1", + HashPath: "newtonsoft.json.13.0.1.nupkg.sha512", }, - { - Name: "System.Diagnostics.DiagnosticSource", - Version: "6.0.0", - PURL: "pkg:nuget/System.Diagnostics.DiagnosticSource@6.0.0", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "System.Diagnostics.DiagnosticSource", - Version: "6.0.0", - Sha512: "sha512-frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==", - Path: "system.diagnostics.diagnosticsource/6.0.0", - HashPath: "system.diagnostics.diagnosticsource.6.0.0.nupkg.sha512", - }, + } + serilogSinksConsole := pkg.Package{ + Name: "Serilog.Sinks.Console", + Version: "4.0.1", + PURL: "pkg:nuget/Serilog.Sinks.Console@4.0.1", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "Serilog.Sinks.Console", + Version: "4.0.1", + Sha512: "sha512-apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", + Path: "serilog.sinks.console/4.0.1", + HashPath: "serilog.sinks.console.4.0.1.nupkg.sha512", }, - { - Name: "System.Runtime.CompilerServices.Unsafe", - Version: "6.0.0", - PURL: "pkg:nuget/System.Runtime.CompilerServices.Unsafe@6.0.0", - Locations: fixtureLocationSet, - Language: pkg.Dotnet, - Type: pkg.DotnetPkg, - MetadataType: pkg.DotnetDepsMetadataType, - Metadata: pkg.DotnetDepsMetadata{ - Name: "System.Runtime.CompilerServices.Unsafe", - Version: "6.0.0", - Sha512: "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", - Path: "system.runtime.compilerservices.unsafe/6.0.0", - HashPath: "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512", - }, + } + serilog := pkg.Package{ + Name: "Serilog", + Version: "2.10.0", + PURL: "pkg:nuget/Serilog@2.10.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "Serilog", + Version: "2.10.0", + Sha512: "sha512-+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==", + Path: "serilog/2.10.0", + HashPath: "serilog.2.10.0.nupkg.sha512", + }, + } + systemDiagnosticsDiagnosticsource := pkg.Package{ + Name: "System.Diagnostics.DiagnosticSource", + Version: "6.0.0", + PURL: "pkg:nuget/System.Diagnostics.DiagnosticSource@6.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "System.Diagnostics.DiagnosticSource", + Version: "6.0.0", + Sha512: "sha512-frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==", + Path: "system.diagnostics.diagnosticsource/6.0.0", + HashPath: "system.diagnostics.diagnosticsource.6.0.0.nupkg.sha512", + }, + } + systemRuntimeCompilerServicesUnsafe := pkg.Package{ + Name: "System.Runtime.CompilerServices.Unsafe", + Version: "6.0.0", + PURL: "pkg:nuget/System.Runtime.CompilerServices.Unsafe@6.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dotnet, + Type: pkg.DotnetPkg, + MetadataType: pkg.DotnetDepsMetadataType, + Metadata: pkg.DotnetDepsMetadata{ + Name: "System.Runtime.CompilerServices.Unsafe", + Version: "6.0.0", + Sha512: "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", + Path: "system.runtime.compilerservices.unsafe/6.0.0", + HashPath: "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512", }, } - var expectedRelationships []artifact.Relationship - pkgtest.TestFileParser(t, fixture, parseDotnetDeps, expected, expectedRelationships) + expectedPkgs := []pkg.Package{ + awssdkcore, + msftDependencyInjection, + msftDependencyInjectionAbstractions, + msftExtensionsLogging, + msftLoggingAbstractions, + msftExtensionsOptions, + msftExtensionsPrimitives, + newtonsoftJson, + serilog, + serilogSinksConsole, + systemDiagnosticsDiagnosticsource, + systemRuntimeCompilerServicesUnsafe, + testCommon, + rootPkg, + } + + // ┌── (✓ = is represented in the test) + // ↓ + // + // ✓ TestLibrary/1.0.0 (project) + // ✓ ├── [a] Microsoft.Extensions.DependencyInjection/6.0.0 [file version: 6.0.21.52210] + // ✓ │ ├── [b] Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0 [file version: 6.0.21.52210] + // ✓ │ └── [c!] System.Runtime.CompilerServices.Unsafe/6.0.0 [NO TARGET INFO] + // ✓ ├── Microsoft.Extensions.Logging/6.0.0 [file version: 6.0.21.52210] + // ✓ │ ├── Microsoft.Extensions.DependencyInjection/6.0.0 ...to [a] + // ✓ │ ├── Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0 ...to [b] + // ✓ │ ├── Microsoft.Extensions.Logging.Abstractions/6.0.0 [file version: 6.0.21.52210] + // ✓ │ ├── Microsoft.Extensions.Options/6.0.0 [file version: 6.0.21.52210] + // ✓ │ │ ├── Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0 ...to [b] + // ✓ │ │ └── Microsoft.Extensions.Primitives/6.0.0 [file version: 6.0.21.52210] + // ✓ │ │ └── System.Runtime.CompilerServices.Unsafe/6.0.0 ...to [c!] + // ✓ │ └── System.Diagnostics.DiagnosticSource/6.0.0 [NO RUNTIME INFO] + // ✓ │ └── System.Runtime.CompilerServices.Unsafe/6.0.0 ...to [c!] + // ✓ ├── Newtonsoft.Json/13.0.1 [file version: 13.0.1.25517] + // ✓ ├── [d] Serilog/2.10.0 [file version: 2.10.0.0] + // ✓ ├── Serilog.Sinks.Console/4.0.1 [file version: 4.0.1.0] + // ✓ │ └── Serilog/2.10.0 ...to [d] + // ✓ └── [e!] TestCommon/1.0.0 [NOT SERVICEABLE / NO SHA] + // ✓ └── AWSSDK.Core/3.7.10.6 [file version: 3.7.10.6] + + expectedRelationships := []artifact.Relationship{ + { + From: awssdkcore, + To: testCommon, + Type: artifact.DependencyOfRelationship, + }, + { + From: msftDependencyInjection, + To: msftExtensionsLogging, + Type: artifact.DependencyOfRelationship, + }, + { + From: msftDependencyInjection, + To: rootPkg, + Type: artifact.DependencyOfRelationship, + }, + { + From: msftDependencyInjectionAbstractions, + To: msftDependencyInjection, + Type: artifact.DependencyOfRelationship, + }, + { + From: msftDependencyInjectionAbstractions, + To: msftExtensionsLogging, + Type: artifact.DependencyOfRelationship, + }, + { + From: msftDependencyInjectionAbstractions, + To: msftExtensionsOptions, + Type: artifact.DependencyOfRelationship, + }, + { + From: msftExtensionsLogging, + To: rootPkg, + Type: artifact.DependencyOfRelationship, + }, + { + From: msftLoggingAbstractions, + To: msftExtensionsLogging, + Type: artifact.DependencyOfRelationship, + }, + { + From: msftExtensionsOptions, + To: msftExtensionsLogging, + Type: artifact.DependencyOfRelationship, + }, + { + From: msftExtensionsPrimitives, + To: msftExtensionsOptions, + Type: artifact.DependencyOfRelationship, + }, + { + From: newtonsoftJson, + To: rootPkg, + Type: artifact.DependencyOfRelationship, + }, + { + From: serilog, + To: serilogSinksConsole, + Type: artifact.DependencyOfRelationship, + }, + { + From: serilog, + To: rootPkg, + Type: artifact.DependencyOfRelationship, + }, + { + From: serilogSinksConsole, + To: rootPkg, + Type: artifact.DependencyOfRelationship, + }, + { + From: systemDiagnosticsDiagnosticsource, + To: msftExtensionsLogging, + Type: artifact.DependencyOfRelationship, + }, + { + From: systemRuntimeCompilerServicesUnsafe, + To: msftDependencyInjection, + Type: artifact.DependencyOfRelationship, + }, + { + From: systemRuntimeCompilerServicesUnsafe, + To: msftExtensionsPrimitives, + Type: artifact.DependencyOfRelationship, + }, + { + From: systemRuntimeCompilerServicesUnsafe, + To: systemDiagnosticsDiagnosticsource, + Type: artifact.DependencyOfRelationship, + }, + { + From: testCommon, + To: rootPkg, + Type: artifact.DependencyOfRelationship, + }, + } + + pkgtest.TestFileParser(t, fixture, parseDotnetDeps, expectedPkgs, expectedRelationships) } diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/TestLibrary.deps.json b/syft/pkg/cataloger/dotnet/test-fixtures/TestLibrary.deps.json index a429ec409..788eb909e 100644 --- a/syft/pkg/cataloger/dotnet/test-fixtures/TestLibrary.deps.json +++ b/syft/pkg/cataloger/dotnet/test-fixtures/TestLibrary.deps.json @@ -232,4 +232,4 @@ "sha512": "" } } -} \ No newline at end of file +} diff --git a/syft/pkg/cataloger/githubactions/cataloger.go b/syft/pkg/cataloger/githubactions/cataloger.go new file mode 100644 index 000000000..825d3942a --- /dev/null +++ b/syft/pkg/cataloger/githubactions/cataloger.go @@ -0,0 +1,16 @@ +package githubactions + +import "github.com/anchore/syft/syft/pkg/cataloger/generic" + +// NewActionUsageCataloger returns GitHub Actions used within workflows and composite actions. +func NewActionUsageCataloger() *generic.Cataloger { + return generic.NewCataloger("github-actions-usage-cataloger"). + WithParserByGlobs(parseWorkflowForActionUsage, "**/.github/workflows/*.yaml", "**/.github/workflows/*.yml"). + WithParserByGlobs(parseCompositeActionForActionUsage, "**/.github/actions/*/action.yml", "**/.github/actions/*/action.yaml") +} + +// NewWorkflowUsageCataloger returns shared workflows used within workflows. +func NewWorkflowUsageCataloger() *generic.Cataloger { + return generic.NewCataloger("github-action-workflow-usage-cataloger"). + WithParserByGlobs(parseWorkflowForWorkflowUsage, "**/.github/workflows/*.yaml", "**/.github/workflows/*.yml") +} diff --git a/syft/pkg/cataloger/githubactions/cataloger_test.go b/syft/pkg/cataloger/githubactions/cataloger_test.go new file mode 100644 index 000000000..f5866ef47 --- /dev/null +++ b/syft/pkg/cataloger/githubactions/cataloger_test.go @@ -0,0 +1,50 @@ +package githubactions + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func TestCataloger_Globs(t *testing.T) { + tests := []struct { + name string + fixture string + cataloger *generic.Cataloger + expected []string + }{ + { + name: "obtain all workflow and composite action files", + fixture: "test-fixtures/glob", + cataloger: NewActionUsageCataloger(), + expected: []string{ + // composite actions + ".github/actions/bootstrap/action.yaml", + ".github/actions/unbootstrap/action.yml", + // workflows + ".github/workflows/release.yml", + ".github/workflows/validations.yaml", + }, + }, + { + name: "obtain all workflow files", + fixture: "test-fixtures/glob", + cataloger: NewWorkflowUsageCataloger(), + expected: []string{ + // workflows + ".github/workflows/release.yml", + ".github/workflows/validations.yaml", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pkgtest.NewCatalogTester(). + FromDirectory(t, test.fixture). + ExpectsResolverContentQueries(test.expected). + TestCataloger(t, test.cataloger) + }) + } +} diff --git a/syft/pkg/cataloger/githubactions/package.go b/syft/pkg/cataloger/githubactions/package.go new file mode 100644 index 000000000..7d6341d3c --- /dev/null +++ b/syft/pkg/cataloger/githubactions/package.go @@ -0,0 +1,103 @@ +package githubactions + +import ( + "strings" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" +) + +func newPackageFromUsageStatement(use string, location file.Location) *pkg.Package { + name, version := parseStepUsageStatement(use) + + if name == "" { + log.WithFields("file", location.RealPath, "statement", use).Trace("unable to parse github action usage statement") + return nil + } + + if strings.Contains(name, ".github/workflows/") { + return newGithubActionWorkflowPackageUsage(name, version, location) + } + + return newGithubActionPackageUsage(name, version, location) +} + +func newGithubActionWorkflowPackageUsage(name, version string, workflowLocation file.Location) *pkg.Package { + p := &pkg.Package{ + Name: name, + Version: version, + Locations: file.NewLocationSet(workflowLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + PURL: packageURL(name, version), + Type: pkg.GithubActionWorkflowPkg, + } + + p.SetID() + + return p +} + +func newGithubActionPackageUsage(name, version string, workflowLocation file.Location) *pkg.Package { + p := &pkg.Package{ + Name: name, + Version: version, + Locations: file.NewLocationSet(workflowLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + PURL: packageURL(name, version), + Type: pkg.GithubActionPkg, + } + + p.SetID() + + return p +} + +func parseStepUsageStatement(use string) (string, string) { + // from octo-org/another-repo/.github/workflows/workflow.yml@v1 get octo-org/another-repo/.github/workflows/workflow.yml and v1 + // from ./.github/workflows/workflow-2.yml interpret as only the name + + // from actions/cache@v3 get actions/cache and v3 + + fields := strings.Split(use, "@") + switch len(fields) { + case 1: + return use, "" + case 2: + return fields[0], fields[1] + } + return "", "" +} + +func packageURL(name, version string) string { + var qualifiers packageurl.Qualifiers + var subPath string + var namespace string + + fields := strings.SplitN(name, "/", 3) + switch len(fields) { + case 1: + return "" + case 2: + namespace = fields[0] + name = fields[1] + case 3: + namespace = fields[0] + name = fields[1] + subPath = fields[2] + } + if namespace == "." { + // this is a local composite action, which is unclear how to represent in a PURL without more information + return "" + } + + // there isn't a github actions PURL but there is a github PURL type for referencing github repos, which is the + // next best thing until there is a supported type. + return packageurl.NewPackageURL( + packageurl.TypeGithub, + namespace, + name, + version, + qualifiers, + subPath, + ).ToString() +} diff --git a/syft/pkg/cataloger/githubactions/parse_composite_action.go b/syft/pkg/cataloger/githubactions/parse_composite_action.go new file mode 100644 index 000000000..0c27e32ce --- /dev/null +++ b/syft/pkg/cataloger/githubactions/parse_composite_action.go @@ -0,0 +1,51 @@ +package githubactions + +import ( + "fmt" + "io" + + "gopkg.in/yaml.v3" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +var _ generic.Parser = parseCompositeActionForActionUsage + +type compositeActionDef struct { + Runs compositeActionRunsDef `yaml:"runs"` +} + +type compositeActionRunsDef struct { + Steps []stepDef `yaml:"steps"` +} + +func parseCompositeActionForActionUsage(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + contents, err := io.ReadAll(reader) + if err != nil { + return nil, nil, fmt.Errorf("unable to read yaml composite action file: %w", err) + } + + var ca compositeActionDef + if err = yaml.Unmarshal(contents, &ca); err != nil { + return nil, nil, fmt.Errorf("unable to parse yaml composite action file: %w", err) + } + + // we use a collection to help with deduplication before raising to higher level processing + pkgs := pkg.NewCollection() + + for _, step := range ca.Runs.Steps { + if step.Uses == "" { + continue + } + + p := newPackageFromUsageStatement(step.Uses, reader.Location) + if p != nil { + pkgs.Add(*p) + } + } + + return pkgs.Sorted(), nil, nil +} diff --git a/syft/pkg/cataloger/githubactions/parse_composite_action_test.go b/syft/pkg/cataloger/githubactions/parse_composite_action_test.go new file mode 100644 index 000000000..e39e18e1a --- /dev/null +++ b/syft/pkg/cataloger/githubactions/parse_composite_action_test.go @@ -0,0 +1,35 @@ +package githubactions + +import ( + "testing" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func Test_parseCompositeActionForActionUsage(t *testing.T) { + fixture := "test-fixtures/composite-action.yaml" + fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + + expected := []pkg.Package{ + { + Name: "actions/setup-go", + Version: "v4", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/setup-go@v4", + }, + { + Name: "actions/cache", + Version: "v3", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/cache@v3", + }, + } + + var expectedRelationships []artifact.Relationship + pkgtest.TestFileParser(t, fixture, parseCompositeActionForActionUsage, expected, expectedRelationships) +} diff --git a/syft/pkg/cataloger/githubactions/parse_workflow.go b/syft/pkg/cataloger/githubactions/parse_workflow.go new file mode 100644 index 000000000..7460d87a7 --- /dev/null +++ b/syft/pkg/cataloger/githubactions/parse_workflow.go @@ -0,0 +1,91 @@ +package githubactions + +import ( + "fmt" + "io" + + "gopkg.in/yaml.v3" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +var ( + _ generic.Parser = parseWorkflowForActionUsage + _ generic.Parser = parseWorkflowForWorkflowUsage +) + +type workflowDef struct { + Jobs map[string]workflowJobDef `yaml:"jobs"` +} + +type workflowJobDef struct { + Uses string `yaml:"uses"` + Steps []stepDef `yaml:"steps"` +} + +type stepDef struct { + Name string `yaml:"name"` + Uses string `yaml:"uses"` + With struct { + Path string `yaml:"path"` + Key string `yaml:"key"` + } `yaml:"with"` +} + +func parseWorkflowForWorkflowUsage(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + contents, err := io.ReadAll(reader) + if err != nil { + return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", err) + } + + var wf workflowDef + if err = yaml.Unmarshal(contents, &wf); err != nil { + return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", err) + } + + // we use a collection to help with deduplication before raising to higher level processing + pkgs := pkg.NewCollection() + + for _, job := range wf.Jobs { + if job.Uses != "" { + p := newPackageFromUsageStatement(job.Uses, reader.Location) + if p != nil { + pkgs.Add(*p) + } + } + } + + return pkgs.Sorted(), nil, nil +} + +func parseWorkflowForActionUsage(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + contents, err := io.ReadAll(reader) + if err != nil { + return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", err) + } + + var wf workflowDef + if err = yaml.Unmarshal(contents, &wf); err != nil { + return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", err) + } + + // we use a collection to help with deduplication before raising to higher level processing + pkgs := pkg.NewCollection() + + for _, job := range wf.Jobs { + for _, step := range job.Steps { + if step.Uses == "" { + continue + } + p := newPackageFromUsageStatement(step.Uses, reader.Location) + if p != nil { + pkgs.Add(*p) + } + } + } + + return pkgs.Sorted(), nil, nil +} diff --git a/syft/pkg/cataloger/githubactions/parse_workflow_test.go b/syft/pkg/cataloger/githubactions/parse_workflow_test.go new file mode 100644 index 000000000..f5e5128b4 --- /dev/null +++ b/syft/pkg/cataloger/githubactions/parse_workflow_test.go @@ -0,0 +1,88 @@ +package githubactions + +import ( + "testing" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func Test_parseWorkflowForActionUsage(t *testing.T) { + fixture := "test-fixtures/workflow-multi-job.yaml" + fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + + expected := []pkg.Package{ + { + Name: "./.github/actions/bootstrap", + Version: "", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "", // don't have enough context without parsing the git origin, which still may not be accurate + }, + { + Name: "actions/cache", + Version: "v3", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/cache@v3", + }, + { + Name: "actions/cache/restore", + Version: "v3", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/cache@v3#restore", + }, + { + Name: "actions/cache/save", + Version: "v3", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/cache@v3#save", + }, + { + Name: "actions/checkout", + Version: "v4", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/checkout@v4", + }, + } + + var expectedRelationships []artifact.Relationship + pkgtest.TestFileParser(t, fixture, parseWorkflowForActionUsage, expected, expectedRelationships) +} + +func Test_parseWorkflowForWorkflowUsage(t *testing.T) { + fixture := "test-fixtures/call-shared-workflow.yaml" + fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + + expected := []pkg.Package{ + { + Name: "octo-org/this-repo/.github/workflows/workflow-1.yml", + Version: "172239021f7ba04fe7327647b213799853a9eb89", + Type: pkg.GithubActionWorkflowPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/octo-org/this-repo@172239021f7ba04fe7327647b213799853a9eb89#.github/workflows/workflow-1.yml", + }, + { + Name: "./.github/workflows/workflow-2.yml", + Version: "", + Type: pkg.GithubActionWorkflowPkg, + Locations: fixtureLocationSet, + PURL: "", // don't have enough context without parsing the git origin, which still may not be accurate + }, + { + Name: "octo-org/another-repo/.github/workflows/workflow.yml", + Version: "v1", + Type: pkg.GithubActionWorkflowPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/octo-org/another-repo@v1#.github/workflows/workflow.yml", + }, + } + + var expectedRelationships []artifact.Relationship + pkgtest.TestFileParser(t, fixture, parseWorkflowForWorkflowUsage, expected, expectedRelationships) +} diff --git a/syft/pkg/cataloger/githubactions/test-fixtures/call-shared-workflow.yaml b/syft/pkg/cataloger/githubactions/test-fixtures/call-shared-workflow.yaml new file mode 100644 index 000000000..69061958b --- /dev/null +++ b/syft/pkg/cataloger/githubactions/test-fixtures/call-shared-workflow.yaml @@ -0,0 +1,19 @@ +jobs: + + call-workflow-1-in-local-repo: + uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89 + + call-workflow-2-in-local-repo: + uses: ./.github/workflows/workflow-2.yml + + call-workflow-in-another-repo: + uses: octo-org/another-repo/.github/workflows/workflow.yml@v1 + + + unit-test: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + + - name: Bootstrap environment + run: make unit diff --git a/syft/pkg/cataloger/githubactions/test-fixtures/composite-action.yaml b/syft/pkg/cataloger/githubactions/test-fixtures/composite-action.yaml new file mode 100644 index 000000000..b294c90ae --- /dev/null +++ b/syft/pkg/cataloger/githubactions/test-fixtures/composite-action.yaml @@ -0,0 +1,81 @@ +name: "Bootstrap" +description: "Bootstrap all tools and dependencies" +inputs: + go-version: + description: "Go version to install" + required: true + default: "1.21.x" + use-go-cache: + description: "Restore go cache" + required: true + default: "true" + cache-key-prefix: + description: "Prefix all cache keys with this value" + required: true + default: "831180ac25" + build-cache-key-prefix: + description: "Prefix build cache key with this value" + required: true + default: "f8b6d31dea" + bootstrap-apt-packages: + description: "Space delimited list of tools to install via apt" + default: "libxml2-utils" + +runs: + using: "composite" + steps: + - uses: actions/setup-go@v4 + with: + go-version: ${{ inputs.go-version }} + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v3 + with: + 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 + shell: bash + if: steps.go-mod-cache.outputs.cache-hit != 'true' && inputs.use-go-cache == 'true' + run: make bootstrap-go + + - name: Install apt packages + if: inputs.bootstrap-apt-packages != '' + shell: bash + run: | + DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y ${{ inputs.bootstrap-apt-packages }} + + - name: Create all cache fingerprints + shell: bash + run: make fingerprints + diff --git a/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/actions/bootstrap/action.yaml b/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/actions/bootstrap/action.yaml new file mode 100644 index 000000000..095e73c82 --- /dev/null +++ b/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/actions/bootstrap/action.yaml @@ -0,0 +1 @@ +# fake \ No newline at end of file diff --git a/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/actions/unbootstrap/action.yml b/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/actions/unbootstrap/action.yml new file mode 100644 index 000000000..095e73c82 --- /dev/null +++ b/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/actions/unbootstrap/action.yml @@ -0,0 +1 @@ +# fake \ No newline at end of file diff --git a/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/workflows/release.yml b/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/workflows/release.yml new file mode 100644 index 000000000..095e73c82 --- /dev/null +++ b/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/workflows/release.yml @@ -0,0 +1 @@ +# fake \ No newline at end of file diff --git a/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/workflows/validations.yaml b/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/workflows/validations.yaml new file mode 100644 index 000000000..095e73c82 --- /dev/null +++ b/syft/pkg/cataloger/githubactions/test-fixtures/glob/.github/workflows/validations.yaml @@ -0,0 +1 @@ +# fake \ No newline at end of file diff --git a/syft/pkg/cataloger/githubactions/test-fixtures/workflow-multi-job.yaml b/syft/pkg/cataloger/githubactions/test-fixtures/workflow-multi-job.yaml new file mode 100644 index 000000000..36502c133 --- /dev/null +++ b/syft/pkg/cataloger/githubactions/test-fixtures/workflow-multi-job.yaml @@ -0,0 +1,210 @@ +name: "Validations" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + + Static-Analysis: + # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline + name: "Static analysis" + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + + - name: Bootstrap environment + uses: ./.github/actions/bootstrap + + - name: Run static analysis + run: make static-analysis + + + Unit-Test: + # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline + name: "Unit tests" + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + + - name: Bootstrap environment + uses: ./.github/actions/bootstrap + + - name: Restore Java test-fixture cache + uses: actions/cache@v3 + with: + path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages + key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint' ) }} + + - name: Restore RPM test-fixture cache + uses: actions/cache@v3 + with: + path: syft/pkg/cataloger/rpm/test-fixtures/rpms + key: ${{ runner.os }}-unit-rpm-cache-${{ hashFiles( 'syft/pkg/cataloger/rpm/test-fixtures/rpms.fingerprint' ) }} + + - name: Restore go binary test-fixture cache + uses: actions/cache@v3 + with: + path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries + key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }} + + - name: Restore binary cataloger test-fixture cache + uses: actions/cache@v3 + with: + path: syft/pkg/cataloger/binary/test-fixtures/classifiers/dynamic + key: ${{ runner.os }}-unit-binary-cataloger-cache-${{ hashFiles( 'syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint' ) }} + + - name: Restore Kernel test-fixture cache + uses: actions/cache@v3 + with: + path: syft/pkg/cataloger/kernel/test-fixtures/cache + key: ${{ runner.os }}-unit-kernel-cache-${{ hashFiles( 'syft/pkg/cataloger/kernel/test-fixtures/cache.fingerprint' ) }} + + - name: Run unit tests + run: make unit + + + 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@v4 + + - name: Bootstrap environment + uses: ./.github/actions/bootstrap + + - name: Validate syft output against the CycloneDX schema + run: make validate-cyclonedx-schema + + - name: Restore integration test cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/test/integration/test-fixtures/cache + key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }} + + - name: Run integration tests + run: make integration + + + Build-Snapshot-Artifacts: + name: "Build snapshot artifacts" + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + + - name: Bootstrap environment + uses: ./.github/actions/bootstrap + with: + # why have another build cache key? We don't want unit/integration/etc test build caches to replace + # the snapshot build cache, which includes builds for all OSs and architectures. As long as this key is + # unique from the build-cache-key-prefix in other CI jobs, we should be fine. + # + # note: ideally this value should match what is used in release (just to help with build times). + build-cache-key-prefix: "snapshot" + bootstrap-apt-packages: "" + + - name: Build snapshot artifacts + run: make snapshot + + # 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 + with: + path: snapshot + key: snapshot-build-${{ github.run_id }} + + + Acceptance-Linux: + # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline + name: "Acceptance tests (Linux)" + needs: [Build-Snapshot-Artifacts] + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + + - name: Download snapshot build + uses: actions/cache/restore@v3 + with: + path: snapshot + key: snapshot-build-${{ github.run_id }} + + - name: Run comparison tests (Linux) + run: make compare-linux + + - name: Restore install.sh test image cache + id: install-test-image-cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/test/install/cache + key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }} + + - name: Load test image cache + if: steps.install-test-image-cache.outputs.cache-hit == 'true' + run: make install-test-cache-load + + - name: Run install.sh tests (Linux) + run: make install-test + + - name: (cache-miss) Create test image cache + if: steps.install-test-image-cache.outputs.cache-hit != 'true' + run: make install-test-cache-save + + + Acceptance-Mac: + # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline + name: "Acceptance tests (Mac)" + needs: [Build-Snapshot-Artifacts] + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Download snapshot build + uses: actions/cache/restore@v3 + 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 + with: + path: image.tar + key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }} + + - name: Run comparison tests (Mac) + run: make compare-mac + + - name: Run install.sh tests (Mac) + run: make install-test-ci-mac + + + Cli-Linux: + # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline + name: "CLI tests (Linux)" + needs: [Build-Snapshot-Artifacts] + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + + - name: Bootstrap environment + uses: ./.github/actions/bootstrap + + - name: Restore CLI test-fixture cache + uses: actions/cache@v3 + 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 + with: + path: snapshot + key: snapshot-build-${{ github.run_id }} + + - name: Run CLI Tests (Linux) + run: make cli diff --git a/syft/pkg/cataloger/golang/cataloger.go b/syft/pkg/cataloger/golang/cataloger.go index ee936da96..d1d117157 100644 --- a/syft/pkg/cataloger/golang/cataloger.go +++ b/syft/pkg/cataloger/golang/cataloger.go @@ -4,14 +4,21 @@ Package golang provides a concrete Cataloger implementation for go.mod files. package golang import ( + "fmt" + "regexp" + "strings" + "github.com/anchore/syft/internal" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/event/monitor" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/generic" ) +var versionCandidateGroups = regexp.MustCompile(`(?P\d+(\.\d+)?(\.\d+)?)(?P\w*)`) + // NewGoModFileCataloger returns a new Go module cataloger object. func NewGoModFileCataloger(opts GoCatalogerOpts) pkg.Cataloger { c := goModCataloger{ @@ -47,5 +54,75 @@ func (p *progressingCataloger) Name() string { func (p *progressingCataloger) Catalog(resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { defer p.progress.SetCompleted() - return p.cataloger.Catalog(resolver) + pkgs, relationships, err := p.cataloger.Catalog(resolver) + goCompilerPkgs := []pkg.Package{} + totalLocations := file.NewLocationSet() + for _, goPkg := range pkgs { + mValue, ok := goPkg.Metadata.(pkg.GolangBinMetadata) + if !ok { + continue + } + // go binary packages should only contain a single location + for _, location := range goPkg.Locations.ToSlice() { + if !totalLocations.Contains(location) { + stdLibPkg := newGoStdLib(mValue.GoCompiledVersion, goPkg.Locations) + if stdLibPkg != nil { + goCompilerPkgs = append(goCompilerPkgs, *stdLibPkg) + totalLocations.Add(location) + } + } + } + } + pkgs = append(pkgs, goCompilerPkgs...) + return pkgs, relationships, err +} +func newGoStdLib(version string, location file.LocationSet) *pkg.Package { + stdlibCpe, err := generateStdlibCpe(version) + if err != nil { + return nil + } + goCompilerPkg := &pkg.Package{ + Name: "stdlib", + Version: version, + PURL: packageURL("stdlib", strings.TrimPrefix(version, "go")), + CPEs: []cpe.CPE{stdlibCpe}, + Locations: location, + Language: pkg.Go, + Type: pkg.GoModulePkg, + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + GoCompiledVersion: version, + }, + } + goCompilerPkg.SetID() + + return goCompilerPkg +} + +func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) { + // GoCompiledVersion when pulled from a binary is prefixed by go + version = strings.TrimPrefix(version, "go") + + // we also need to trim starting from the first + to + // correctly extract potential rc candidate information for cpe generation + // ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found then + is returned + after, _, found := strings.Cut("+", version) + if found { + version = after + } + + // extracting and + // https://regex101.com/r/985GsI/1 + captureGroups := internal.MatchNamedCaptureGroups(versionCandidateGroups, version) + vr, ok := captureGroups["version"] + if !ok || vr == "" { + return stdlibCpe, fmt.Errorf("could not match candidate version for: %s", version) + } + + cpeString := fmt.Sprintf("cpe:2.3:a:golang:go:%s:-:*:*:*:*:*:*", captureGroups["version"]) + if candidate, ok := captureGroups["candidate"]; ok && candidate != "" { + cpeString = fmt.Sprintf("cpe:2.3:a:golang:go:%s:%s:*:*:*:*:*:*", vr, candidate) + } + + return cpe.New(cpeString) } diff --git a/syft/pkg/cataloger/golang/cataloger_test.go b/syft/pkg/cataloger/golang/cataloger_test.go index 7323e9fa8..b1e26ba35 100644 --- a/syft/pkg/cataloger/golang/cataloger_test.go +++ b/syft/pkg/cataloger/golang/cataloger_test.go @@ -3,6 +3,9 @@ package golang import ( "testing" + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) @@ -56,3 +59,30 @@ func Test_Binary_Cataloger_Globs(t *testing.T) { }) } } + +func Test_Binary_Cataloger_Stdlib_Cpe(t *testing.T) { + tests := []struct { + name string + candidate string + want string + }{ + { + name: "generateStdlibCpe generates a cpe with a - for a major version", + candidate: "go1.21.0", + want: "cpe:2.3:a:golang:go:1.21.0:-:*:*:*:*:*:*", + }, + { + name: "generateStdlibCpe generates a cpe with an rc candidate for a major rc version", + candidate: "go1.21rc2", + want: "cpe:2.3:a:golang:go:1.21:rc2:*:*:*:*:*:*", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := generateStdlibCpe(tc.candidate) + assert.NoError(t, err, "expected no err; got %v", err) + assert.Equal(t, cpe.String(got), tc.want) + }) + } +} diff --git a/syft/pkg/cataloger/golang/package.go b/syft/pkg/cataloger/golang/package.go index 30ba083b4..37942563b 100644 --- a/syft/pkg/cataloger/golang/package.go +++ b/syft/pkg/cataloger/golang/package.go @@ -11,7 +11,7 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func (c *goBinaryCataloger) newGoBinaryPackage(resolver file.Resolver, dep *debug.Module, mainModule, goVersion, architecture string, buildSettings map[string]string, locations ...file.Location) pkg.Package { +func (c *goBinaryCataloger) newGoBinaryPackage(resolver file.Resolver, dep *debug.Module, mainModule, goVersion, architecture string, buildSettings map[string]string, cryptoSettings []string, locations ...file.Location) pkg.Package { if dep.Replace != nil { dep = dep.Replace } @@ -36,6 +36,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(resolver file.Resolver, dep *debu Architecture: architecture, BuildSettings: buildSettings, MainModule: mainModule, + GoCryptoSettings: cryptoSettings, }, } diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index 89ed4ba07..5ccaaae3d 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -16,7 +16,6 @@ import ( "golang.org/x/mod/module" "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" @@ -57,16 +56,17 @@ func (c *goBinaryCataloger) parseGoBinary(resolver file.Resolver, _ *generic.Env return nil, nil, err } - mods, archs := scanFile(unionReader, reader.RealPath) + mods := scanFile(unionReader, reader.RealPath) internal.CloseAndLogError(reader.ReadCloser, reader.RealPath) - for i, mod := range mods { - pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, archs[i])...) + for _, mod := range mods { + pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch)...) } + return pkgs, nil, nil } -func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *debug.BuildInfo, arch string, location file.Location) pkg.Package { +func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location) pkg.Package { gbs := getBuildSettings(mod.Settings) main := c.newGoBinaryPackage( resolver, @@ -75,6 +75,7 @@ func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *debug mod.GoVersion, arch, gbs, + mod.cryptoSettings, location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), ) @@ -150,42 +151,6 @@ func extractVersionFromLDFlags(ldflags string) (majorVersion string, fullVersion return "", "" } -// getArchs finds a binary architecture by two ways: -// 1) reading build info from binaries compiled by go1.18+ -// 2) reading file headers from binaries compiled by < go1.18 -func getArchs(readers []io.ReaderAt, builds []*debug.BuildInfo) []string { - if len(readers) != len(builds) { - log.Trace("golang cataloger: bin parsing: number of builds and readers doesn't match") - return nil - } - - if len(readers) == 0 || len(builds) == 0 { - log.Tracef("golang cataloger: bin parsing: %d readers and %d build info items", len(readers), len(builds)) - return nil - } - - archs := make([]string, len(builds)) - for i, build := range builds { - archs[i] = getGOARCH(build.Settings) - } - - // if architecture was found via build settings return - if archs[0] != "" { - return archs - } - - for i, r := range readers { - a, err := getGOARCHFromBin(r) - if err != nil { - log.Tracef("golang cataloger: bin parsing: getting arch from binary: %v", err) - continue - } - - archs[i] = a - } - return archs -} - func getGOARCH(settings []debug.BuildSetting) string { for _, s := range settings { if s.Key == GOARCH { @@ -255,7 +220,7 @@ func createMainModuleFromPath(path string) (mod debug.Module) { return } -func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *debug.BuildInfo, arch string) []pkg.Package { +func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string) []pkg.Package { var pkgs []pkg.Package if mod == nil { return pkgs @@ -277,6 +242,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file mod.GoVersion, arch, nil, + mod.cryptoSettings, location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), ) if pkg.IsValid(&p) { diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index d578b46ad..6b19a5465 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -155,27 +155,25 @@ func TestBuildGoPkgInfo(t *testing.T) { tests := []struct { name string - mod *debug.BuildInfo - arch string + mod *extendedBuildInfo expected []pkg.Package }{ - { - name: "parse an empty mod", - mod: nil, - expected: []pkg.Package(nil), - }, { name: "package without name", - mod: &debug.BuildInfo{ - Deps: []*debug.Module{ - { - Path: "github.com/adrg/xdg", - }, - { - Path: "", - Version: "v0.2.1", + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + Deps: []*debug.Module{ + { + Path: "github.com/adrg/xdg", + }, + { + Path: "", + Version: "v0.2.1", + }, }, }, + cryptoSettings: nil, + arch: "", }, expected: []pkg.Package{ { @@ -198,26 +196,29 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "buildGoPkgInfo parses a blank mod and returns no packages", - mod: &debug.BuildInfo{}, + mod: &extendedBuildInfo{&debug.BuildInfo{}, nil, ""}, expected: []pkg.Package(nil), }, { name: "parse a mod without main module", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, - }, - Deps: []*debug.Module{ - { - Path: "github.com/adrg/xdg", - Version: "v0.2.1", - Sum: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=", + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + }, + Deps: []*debug.Module{ + { + Path: "github.com/adrg/xdg", + Version: "v0.2.1", + Sum: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=", + }, }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{ { @@ -245,15 +246,18 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "parse a mod with path but no main module", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + }, + Path: "github.com/a/b/c", }, - Path: "github.com/a/b/c", + cryptoSettings: []string{"boringcrypto + fips"}, + arch: archDetails, }, expected: []pkg.Package{ { @@ -280,39 +284,46 @@ func TestBuildGoPkgInfo(t *testing.T) { "GOARCH": "amd64", "GOOS": "darwin", }, - MainModule: "github.com/a/b/c", + MainModule: "github.com/a/b/c", + GoCryptoSettings: []string{"boringcrypto + fips"}, }, }, }, }, { name: "parse a mod without packages", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{unmodifiedMain}, }, { name: "parse main mod and replace devel pseudo version and ldflags exists (but contains no version)", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, - {Key: "vcs.revision", Value: "41bc6bb410352845f22766e27dd48ba93aa825a4"}, - {Key: "vcs.time", Value: "2022-10-14T19:54:57Z"}, - {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X blah=foobar`}, + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + {Key: "vcs.revision", Value: "41bc6bb410352845f22766e27dd48ba93aa825a4"}, + {Key: "vcs.time", Value: "2022-10-14T19:54:57Z"}, + {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X blah=foobar`}, + }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{ { @@ -348,18 +359,21 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "parse main mod and replace devel version with one from ldflags with vcs. build settings", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, - {Key: "vcs.revision", Value: "41bc6bb410352845f22766e27dd48ba93aa825a4"}, - {Key: "vcs.time", Value: "2022-10-14T19:54:57Z"}, - {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X github.com/anchore/syft/internal/version.version=0.79.0`}, + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + {Key: "vcs.revision", Value: "41bc6bb410352845f22766e27dd48ba93aa825a4"}, + {Key: "vcs.time", Value: "2022-10-14T19:54:57Z"}, + {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X github.com/anchore/syft/internal/version.version=0.79.0`}, + }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{ { @@ -395,16 +409,19 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "parse main mod and replace devel version with one from ldflags without any vcs. build settings", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, - {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X github.com/anchore/syft/internal/version.version=0.79.0`}, + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X github.com/anchore/syft/internal/version.version=0.79.0`}, + }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{ { @@ -438,16 +455,19 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "parse main mod and replace devel version with one from ldflags main.version without any vcs. build settings", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, - {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X main.version=0.79.0`}, + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X main.version=0.79.0`}, + }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{ { @@ -481,16 +501,19 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "parse main mod and replace devel version with one from ldflags main.Version without any vcs. build settings", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, - {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X main.Version=0.79.0`}, + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + {Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X main.Version=0.79.0`}, + }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{ { @@ -524,17 +547,20 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "parse main mod and replace devel version with a pseudo version", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, - {Key: "vcs.revision", Value: "41bc6bb410352845f22766e27dd48ba93aa825a4"}, - {Key: "vcs.time", Value: "2022-10-14T19:54:57Z"}, + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + {Key: "vcs.revision", Value: "41bc6bb410352845f22766e27dd48ba93aa825a4"}, + {Key: "vcs.time", Value: "2022-10-14T19:54:57Z"}, + }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{ { @@ -569,27 +595,30 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "parse a populated mod string and returns packages but no source info", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, - }, - Deps: []*debug.Module{ - { - Path: "github.com/adrg/xdg", - Version: "v0.2.1", - Sum: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=", + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, }, - { - Path: "github.com/anchore/client-go", - Version: "v0.0.0-20210222170800-9c70f9b80bcf", - Sum: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=", + Deps: []*debug.Module{ + { + Path: "github.com/adrg/xdg", + Version: "v0.2.1", + Sum: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=", + }, + { + Path: "github.com/anchore/client-go", + Version: "v0.0.0-20210222170800-9c70f9b80bcf", + Sum: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=", + }, }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{ { @@ -641,32 +670,35 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "parse a populated mod string and returns packages when a replace directive exists", - arch: archDetails, - mod: &debug.BuildInfo{ - GoVersion: goCompiledVersion, - Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, - Settings: []debug.BuildSetting{ - {Key: "GOARCH", Value: archDetails}, - {Key: "GOOS", Value: "darwin"}, - {Key: "GOAMD64", Value: "v1"}, - }, - Deps: []*debug.Module{ - { - Path: "golang.org/x/sys", - Version: "v0.0.0-20211006194710-c8a6f5223071", - Sum: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=", + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: goCompiledVersion, + Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, }, - { - Path: "golang.org/x/term", - Version: "v0.0.0-20210927222741-03fcf44c2211", - Sum: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=", - Replace: &debug.Module{ + Deps: []*debug.Module{ + { + Path: "golang.org/x/sys", + Version: "v0.0.0-20211006194710-c8a6f5223071", + Sum: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=", + }, + { Path: "golang.org/x/term", - Version: "v0.0.0-20210916214954-140adaaadfaf", - Sum: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=", + Version: "v0.0.0-20210927222741-03fcf44c2211", + Sum: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=", + Replace: &debug.Module{ + Path: "golang.org/x/term", + Version: "v0.0.0-20210916214954-140adaaadfaf", + Sum: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=", + }, }, }, }, + cryptoSettings: nil, + arch: archDetails, }, expected: []pkg.Package{ { @@ -731,7 +763,7 @@ func TestBuildGoPkgInfo(t *testing.T) { ) c := goBinaryCataloger{} - pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.arch) + pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch) assert.Equal(t, test.expected, pkgs) }) } diff --git a/syft/pkg/cataloger/golang/scan_binary.go b/syft/pkg/cataloger/golang/scan_binary.go index c010b2a88..892729929 100644 --- a/syft/pkg/cataloger/golang/scan_binary.go +++ b/syft/pkg/cataloger/golang/scan_binary.go @@ -6,36 +6,82 @@ import ( "io" "runtime/debug" + "github.com/kastenhq/goversion/version" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader" ) +type extendedBuildInfo struct { + *debug.BuildInfo + cryptoSettings []string + arch string +} + // scanFile scans file to try to report the Go and module versions. -func scanFile(reader unionreader.UnionReader, filename string) ([]*debug.BuildInfo, []string) { +func scanFile(reader unionreader.UnionReader, filename string) []*extendedBuildInfo { // NOTE: multiple readers are returned to cover universal binaries, which are files // with more than one binary readers, err := unionreader.GetReaders(reader) if err != nil { log.WithFields("error", err).Warnf("failed to open a golang binary") - return nil, nil + return nil } - var builds []*debug.BuildInfo + var builds []*extendedBuildInfo for _, r := range readers { bi, err := getBuildInfo(r) if err != nil { log.WithFields("file", filename, "error", err).Trace("unable to read golang buildinfo") continue } + // it's possible the reader just isn't a go binary, in which case just skip it if bi == nil { continue } - builds = append(builds, bi) + + v, err := getCryptoInformation(r) + if err != nil { + log.WithFields("file", filename, "error", err).Trace("unable to read golang version info") + // don't skip this build info. + // we can still catalog packages, even if we can't get the crypto information + } + arch := getGOARCH(bi.Settings) + if arch == "" { + arch, err = getGOARCHFromBin(r) + if err != nil { + log.WithFields("file", filename, "error", err).Trace("unable to read golang arch info") + // don't skip this build info. + // we can still catalog packages, even if we can't get the arch information + } + } + + builds = append(builds, &extendedBuildInfo{bi, v, arch}) + } + return builds +} + +func getCryptoInformation(reader io.ReaderAt) ([]string, error) { + v, err := version.ReadExeFromReader(reader) + if err != nil { + return nil, err } - archs := getArchs(readers, builds) + return getCryptoSettingsFromVersion(v), nil +} - return builds, archs +func getCryptoSettingsFromVersion(v version.Version) []string { + cryptoSettings := []string{} + if v.StandardCrypto { + cryptoSettings = append(cryptoSettings, "standard-crypto") + } + if v.BoringCrypto { + cryptoSettings = append(cryptoSettings, "boring-crypto") + } + if v.FIPSOnly { + cryptoSettings = append(cryptoSettings, "crypto/tls/fipsonly") + } + return cryptoSettings } func getBuildInfo(r io.ReaderAt) (bi *debug.BuildInfo, err error) { diff --git a/syft/pkg/cataloger/golang/scan_binary_test.go b/syft/pkg/cataloger/golang/scan_binary_test.go index 8a5541137..a1c8dad7e 100644 --- a/syft/pkg/cataloger/golang/scan_binary_test.go +++ b/syft/pkg/cataloger/golang/scan_binary_test.go @@ -6,6 +6,7 @@ import ( "runtime/debug" "testing" + "github.com/kastenhq/goversion/version" "github.com/stretchr/testify/assert" ) @@ -38,3 +39,72 @@ func Test_getBuildInfo(t *testing.T) { }) } } + +func Test_getCryptoSettingsFromVersion(t *testing.T) { + for _, tt := range []struct { + name string + version version.Version + result []string + }{ + { + name: "standard crypto", + version: version.Version{ + StandardCrypto: true, + }, + result: []string{"standard-crypto"}, + }, + { + name: "boring crypto", + version: version.Version{ + BoringCrypto: true, + }, + result: []string{"boring-crypto"}, + }, + { // Should never see this. Boring crypto is required for fipsonly + name: "fipsonly", + version: version.Version{ + FIPSOnly: true, + }, + result: []string{"crypto/tls/fipsonly"}, + }, + { + name: "boring crypto and fipsonly", + version: version.Version{ + BoringCrypto: true, + FIPSOnly: true, + }, + result: []string{"boring-crypto", "crypto/tls/fipsonly"}, + }, + { // Should never see this. + name: "boring and standard crypto!", + version: version.Version{ + BoringCrypto: true, + StandardCrypto: true, + }, + result: []string{"boring-crypto", "standard-crypto"}, + }, + { // Should never see this. Boring crypto is required for fipsonly + name: "fipsonly and standard crypto!", + version: version.Version{ + FIPSOnly: true, + StandardCrypto: true, + }, + result: []string{"crypto/tls/fipsonly", "standard-crypto"}, + }, + + { // Should never see this. Boring crypto is required for fipsonly + name: "fipsonly boringcrypto and standard crypto!", + version: version.Version{ + FIPSOnly: true, + StandardCrypto: true, + BoringCrypto: true, + }, + result: []string{"crypto/tls/fipsonly", "standard-crypto", "boring-crypto"}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + res := getCryptoSettingsFromVersion(tt.version) + assert.ElementsMatch(t, res, tt.result) + }) + } +} diff --git a/syft/pkg/cataloger/golang/test-fixtures/go-sum-hashes/go.sum b/syft/pkg/cataloger/golang/test-fixtures/go-sum-hashes/go.sum index 4a2cebfd6..f87b23fd2 100644 --- a/syft/pkg/cataloger/golang/test-fixtures/go-sum-hashes/go.sum +++ b/syft/pkg/cataloger/golang/test-fixtures/go-sum-hashes/go.sum @@ -1,7 +1,18 @@ +github.com/CycloneDX/cyclonedx-go v0.6.0/go.mod h1:nQCiF4Tvrg5Ieu8qPhYMvzPGMu5I7fANZkrSsJjl5mg= github.com/CycloneDX/cyclonedx-go v0.7.0 h1:jNxp8hL7UpcvPDFXjY+Y1ibFtsW+e5zyF9QoSmhK/zg= github.com/CycloneDX/cyclonedx-go v0.7.0/go.mod h1:W5Z9w8pTTL+t+yG3PCiFRGlr8PUlE0pGWzKSJbsyXkg= 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/bradleyjkemp/cupaloy/v2 v2.7.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go index 573cc5bee..e5b44d459 100644 --- a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go +++ b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/sanity-io/litter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -252,6 +253,23 @@ func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) { } } +var relationshipStringer = litter.Options{ + Compact: true, + StripPackageNames: false, + HidePrivateFields: true, // we want to ignore package IDs + HideZeroValues: true, + StrictGo: true, + //FieldExclusions: ... // these can be added for future values that need to be ignored + //FieldFilter: ... +} + +func relationshipLess(x, y artifact.Relationship) bool { + // we just need a stable sort, the ordering does not need to be sensible + xStr := relationshipStringer.Sdump(x) + yStr := relationshipStringer.Sdump(y) + return xStr < yStr +} + // nolint:funlen func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) { t.Helper() @@ -259,6 +277,7 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi p.compareOptions = append(p.compareOptions, cmpopts.IgnoreFields(pkg.Package{}, "id"), // note: ID is not deterministic for test purposes cmpopts.SortSlices(pkg.Less), + cmpopts.SortSlices(relationshipLess), cmp.Comparer( func(x, y file.LocationSet) bool { xs := x.ToSlice() @@ -310,6 +329,10 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi opts = append(opts, p.compareOptions...) opts = append(opts, cmp.Reporter(&r)) + // order should not matter + pkg.Sort(p.expectedPkgs) + pkg.Sort(pkgs) + if diff := cmp.Diff(p.expectedPkgs, pkgs, opts...); diff != "" { t.Log("Specific Differences:\n" + r.String()) t.Errorf("unexpected packages from parsing (-expected +actual)\n%s", diff) @@ -322,6 +345,10 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi opts = append(opts, p.compareOptions...) opts = append(opts, cmp.Reporter(&r)) + // order should not matter + pkg.SortRelationships(p.expectedRelationships) + pkg.SortRelationships(relationships) + if diff := cmp.Diff(p.expectedRelationships, relationships, opts...); diff != "" { t.Log("Specific Differences:\n" + r.String()) diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index ea216e906..9e4445f6c 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -8,6 +8,7 @@ import ( "strings" intFile "github.com/anchore/syft/internal/file" + "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" @@ -62,11 +63,11 @@ func parseJavaArchive(_ file.Resolver, _ *generic.Environment, reader file.Locat } // uniquePkgKey creates a unique string to identify the given package. -func uniquePkgKey(p *pkg.Package) string { +func uniquePkgKey(groupID string, p *pkg.Package) string { if p == nil { return "" } - return fmt.Sprintf("%s|%s", p.Name, p.Version) + return fmt.Sprintf("%s|%s|%s", groupID, p.Name, p.Version) } // newJavaArchiveParser returns a new java archive parser object for the given archive. Can be configured to discover @@ -149,7 +150,6 @@ func (j *archiveParser) parse() ([]pkg.Package, []artifact.Relationship, error) // discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages. func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) { // search and parse java manifest files - // TODO: do we want to prefer or check for pom files over manifest here? manifestMatches := j.fileManifest.GlobMatch(manifestGlob) if len(manifestMatches) > 1 { return nil, fmt.Errorf("found multiple manifests in the jar: %+v", manifestMatches) @@ -172,23 +172,21 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) { return nil, nil } - archiveCloser, err := os.Open(j.archivePath) - if err != nil { - return nil, fmt.Errorf("unable to open archive path (%s): %w", j.archivePath, err) - } - defer archiveCloser.Close() - // grab and assign digest for the entire archive - digests, err := intFile.NewDigestsFromFile(archiveCloser, javaArchiveHashes) + digests, err := getDigestsFromArchive(j.archivePath) if err != nil { - log.Warnf("failed to create digest for file=%q: %+v", j.archivePath, err) + return nil, err + } + + licenses, name, version, err := j.parseLicenses(manifest) + if err != nil { + return nil, err } - // we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest - licenses := pkg.NewLicensesFromLocation(j.location, selectLicenses(manifest)...) return &pkg.Package{ - Name: selectName(manifest, j.fileInfo), - Version: selectVersion(manifest, j.fileInfo), + // TODO: maybe select name should just have a pom properties in it? + Name: name, + Version: version, Language: pkg.Java, Licenses: pkg.NewLicenseSet(licenses...), Locations: file.NewLocationSet( @@ -204,6 +202,80 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) { }, nil } +func (j *archiveParser) parseLicenses(manifest *pkg.JavaManifest) ([]pkg.License, string, string, error) { + // we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest + // TODO: when we support locations of paths within archives we should start passing the specific manifest location object instead of the top jar + licenses := pkg.NewLicensesFromLocation(j.location, selectLicenses(manifest)...) + /* + We should name and version from, in this order: + 1. pom.properties if we find exactly 1 + 2. pom.xml if we find exactly 1 + 3. manifest + 4. filename + */ + name, version, pomLicenses := j.guessMainPackageNameAndVersionFromPomInfo() + if name == "" { + name = selectName(manifest, j.fileInfo) + } + if version == "" { + version = selectVersion(manifest, j.fileInfo) + } + if len(licenses) == 0 { + // Today we don't have a way to distinguish between licenses from the manifest and licenses from the pom.xml + // until the file.Location object can support sub-paths (i.e. paths within archives, recursively; issue https://github.com/anchore/syft/issues/2211). + // Until then it's less confusing to use the licenses from the pom.xml only if the manifest did not list any. + licenses = append(licenses, pomLicenses...) + } + + if len(licenses) == 0 { + fileLicenses, err := j.getLicenseFromFileInArchive() + if err != nil { + return nil, "", "", err + } + if fileLicenses != nil { + licenses = append(licenses, fileLicenses...) + } + } + + return licenses, name, version, nil +} + +type parsedPomProject struct { + *pkg.PomProject + Licenses []pkg.License +} + +func (j *archiveParser) guessMainPackageNameAndVersionFromPomInfo() (name, version string, licenses []pkg.License) { + pomPropertyMatches := j.fileManifest.GlobMatch(pomPropertiesGlob) + pomMatches := j.fileManifest.GlobMatch(pomXMLGlob) + var pomPropertiesObject pkg.PomProperties + var pomProjectObject parsedPomProject + if len(pomPropertyMatches) == 1 || len(pomMatches) == 1 { + // we have exactly 1 pom.properties or pom.xml in the archive; assume it represents the + // package we're scanning if the names seem like a plausible match + properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, pomPropertyMatches) + projects, _ := pomProjectByParentPath(j.archivePath, j.location, pomMatches) + + for parentPath, propertiesObj := range properties { + if propertiesObj.ArtifactID != "" && j.fileInfo.name != "" && strings.HasPrefix(propertiesObj.ArtifactID, j.fileInfo.name) { + pomPropertiesObject = propertiesObj + if proj, exists := projects[parentPath]; exists { + pomProjectObject = proj + } + } + } + } + name = pomPropertiesObject.ArtifactID + if name == "" && pomProjectObject.PomProject != nil { + name = pomProjectObject.ArtifactID + } + version = pomPropertiesObject.Version + if version == "" && pomProjectObject.PomProject != nil { + version = pomProjectObject.Version + } + return name, version, pomProjectObject.Licenses +} + // discoverPkgsFromAllMavenFiles parses Maven POM properties/xml for a given // parent package, returning all listed Java packages found for each pom // properties discovered and potentially updating the given parentPkg with new @@ -228,7 +300,7 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([ } for parentPath, propertiesObj := range properties { - var pomProject *pkg.PomProject + var pomProject *parsedPomProject if proj, exists := projects[parentPath]; exists { pomProject = &proj } @@ -242,6 +314,54 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([ return pkgs, nil } +func getDigestsFromArchive(archivePath string) ([]file.Digest, error) { + archiveCloser, err := os.Open(archivePath) + if err != nil { + return nil, fmt.Errorf("unable to open archive path (%s): %w", archivePath, err) + } + defer archiveCloser.Close() + + // grab and assign digest for the entire archive + digests, err := intFile.NewDigestsFromFile(archiveCloser, javaArchiveHashes) + if err != nil { + log.Warnf("failed to create digest for file=%q: %+v", archivePath, err) + } + + return digests, nil +} + +func (j *archiveParser) getLicenseFromFileInArchive() ([]pkg.License, error) { + var fileLicenses []pkg.License + for _, filename := range licenses.FileNames { + licenseMatches := j.fileManifest.GlobMatch("/META-INF/" + filename) + if len(licenseMatches) == 0 { + // Try the root directory if it's not in META-INF + licenseMatches = j.fileManifest.GlobMatch("/" + filename) + } + + if len(licenseMatches) > 0 { + contents, err := intFile.ContentsFromZip(j.archivePath, licenseMatches...) + if err != nil { + return nil, fmt.Errorf("unable to extract java license (%s): %w", j.location, err) + } + + for _, licenseMatch := range licenseMatches { + licenseContents := contents[licenseMatch] + parsed, err := licenses.Parse(strings.NewReader(licenseContents), j.location) + if err != nil { + return nil, err + } + + if len(parsed) > 0 { + fileLicenses = append(fileLicenses, parsed...) + } + } + } + } + + return fileLicenses, nil +} + func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) { // we know that all java archives are zip formatted files, so we can use the shared zip helper return discoverPkgsFromZip(j.location, j.archivePath, j.contentPath, j.fileManifest, parentPkg) @@ -343,15 +463,16 @@ func pomPropertiesByParentPath(archivePath string, location file.Location, extra return propertiesByParentPath, nil } -func pomProjectByParentPath(archivePath string, location file.Location, extractPaths []string) (map[string]pkg.PomProject, error) { +func pomProjectByParentPath(archivePath string, location file.Location, extractPaths []string) (map[string]parsedPomProject, error) { contentsOfMavenProjectFiles, err := intFile.ContentsFromZip(archivePath, extractPaths...) if err != nil { return nil, fmt.Errorf("unable to extract maven files: %w", err) } - projectByParentPath := make(map[string]pkg.PomProject) + projectByParentPath := make(map[string]parsedPomProject) for filePath, fileContents := range contentsOfMavenProjectFiles { - pomProject, err := parsePomXMLProject(filePath, strings.NewReader(fileContents)) + // TODO: when we support locations of paths within archives we should start passing the specific pom.xml location object instead of the top jar + pomProject, err := parsePomXMLProject(filePath, strings.NewReader(fileContents), location) if err != nil { log.WithFields("contents-path", filePath, "location", location.AccessPath()).Warnf("failed to parse pom.xml: %+v", err) continue @@ -371,30 +492,51 @@ func pomProjectByParentPath(archivePath string, location file.Location, extractP return projectByParentPath, nil } -// packagesFromPomProperties processes a single Maven POM properties for a given parent package, returning all listed Java packages found and +// newPackageFromMavenData processes a single Maven POM properties for a given parent package, returning all listed Java packages found and // associating each discovered package to the given parent package. Note the pom.xml is optional, the pom.properties is not. -func newPackageFromMavenData(pomProperties pkg.PomProperties, pomProject *pkg.PomProject, parentPkg *pkg.Package, location file.Location) *pkg.Package { +func newPackageFromMavenData(pomProperties pkg.PomProperties, parsedPomProject *parsedPomProject, parentPkg *pkg.Package, location file.Location) *pkg.Package { // keep the artifact name within the virtual path if this package does not match the parent package vPathSuffix := "" - if !strings.HasPrefix(pomProperties.ArtifactID, parentPkg.Name) { - vPathSuffix += ":" + pomProperties.ArtifactID + groupID := "" + if parentMetadata, ok := parentPkg.Metadata.(pkg.JavaMetadata); ok { + groupID = groupIDFromJavaMetadata(parentPkg.Name, parentMetadata) + } + + parentKey := fmt.Sprintf("%s:%s:%s", groupID, parentPkg.Name, parentPkg.Version) + // Since we don't have a package yet, it's important to use the same `field: value` association that we used when creating the parent package + // See below where Name => pomProperties.ArtifactID and Version => pomProperties.Version. We want to check for potentially nested identical + // packages and create equal virtual paths so they are de duped in the future + pomProjectKey := fmt.Sprintf("%s:%s:%s", pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version) + if parentKey != pomProjectKey { + // build a new virtual path suffix for the package that is different from the parent package + // we want to use the GroupID and ArtifactID here to preserve uniqueness + // Some packages have the same name but different group IDs (e.g. "org.glassfish.jaxb/jaxb-core", "com.sun.xml.bind/jaxb-core") + // https://github.com/anchore/syft/issues/1944 + vPathSuffix += ":" + pomProperties.GroupID + ":" + pomProperties.ArtifactID } virtualPath := location.AccessPath() + vPathSuffix - // discovered props = new package + var pkgPomProject *pkg.PomProject + licenses := make([]pkg.License, 0) + if parsedPomProject != nil { + pkgPomProject = parsedPomProject.PomProject + licenses = append(licenses, parsedPomProject.Licenses...) + } + p := pkg.Package{ Name: pomProperties.ArtifactID, Version: pomProperties.Version, Locations: file.NewLocationSet( location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), ), + Licenses: pkg.NewLicenseSet(licenses...), Language: pkg.Java, Type: pomProperties.PkgTypeIndicated(), MetadataType: pkg.JavaMetadataType, Metadata: pkg.JavaMetadata{ VirtualPath: virtualPath, PomProperties: &pomProperties, - PomProject: pomProject, + PomProject: pkgPomProject, Parent: parentPkg, }, } @@ -408,21 +550,26 @@ func newPackageFromMavenData(pomProperties pkg.PomProperties, pomProject *pkg.Po } func packageIdentitiesMatch(p pkg.Package, parentPkg *pkg.Package) bool { - // the name/version pair matches... - if uniquePkgKey(&p) == uniquePkgKey(parentPkg) { - return true - } - metadata, ok := p.Metadata.(pkg.JavaMetadata) - if !ok { - log.WithFields("package", p.String()).Warn("unable to extract java metadata to check for matching package identity") - return false + parentMetadata, parentOk := parentPkg.Metadata.(pkg.JavaMetadata) + if !ok || !parentOk { + switch { + case !ok: + log.WithFields("package", p.String()).Trace("unable to extract java metadata to check for matching package identity for package: %s", p.Name) + case !parentOk: + log.WithFields("package", parentPkg.String()).Trace("unable to extract java metadata to check for matching package identity for package: %s", parentPkg.Name) + } + // if we can't extract metadata, we can check for matching identities via the package name + // this is not ideal, but it's better than nothing - this should not be used if we have Metadata + + return uniquePkgKey("", &p) == uniquePkgKey("", parentPkg) } - parentMetadata, ok := parentPkg.Metadata.(pkg.JavaMetadata) - if !ok { - log.WithFields("package", p.String()).Warn("unable to extract java metadata from parent for verifying virtual path") - return false + // try to determine identity with the metadata + groupID := groupIDFromJavaMetadata(p.Name, metadata) + parentGroupID := groupIDFromJavaMetadata(parentPkg.Name, parentMetadata) + if uniquePkgKey(groupID, &p) == uniquePkgKey(parentGroupID, parentPkg) { + return true } // the virtual path matches... @@ -434,10 +581,14 @@ func packageIdentitiesMatch(p pkg.Package, parentPkg *pkg.Package) bool { // note: you CANNOT use name-is-subset-of-artifact-id or vice versa --this is too generic. Shaded jars are a good // example of this: where the package name is "cloudbees-analytics-segment-driver" and a child is "analytics", but // they do not indicate the same package. - if metadata.PomProperties.ArtifactID != "" && parentPkg.Name == metadata.PomProperties.ArtifactID { - return true + // NOTE: artifactId might not be a good indicator of uniqueness since archives can contain forks with the same name + // from different groups (e.g. "org.glassfish.jaxb.jaxb-core" and "com.sun.xml.bind.jaxb-core") + // we will use this check as a last resort + if metadata.PomProperties != nil { + if metadata.PomProperties.ArtifactID != "" && parentPkg.Name == metadata.PomProperties.ArtifactID { + return true + } } - return false } diff --git a/syft/pkg/cataloger/java/archive_parser_test.go b/syft/pkg/cataloger/java/archive_parser_test.go index 422de7d48..4ee463159 100644 --- a/syft/pkg/cataloger/java/archive_parser_test.go +++ b/syft/pkg/cataloger/java/archive_parser_test.go @@ -6,7 +6,6 @@ import ( "io" "os" "os/exec" - "path" "path/filepath" "strings" "syscall" @@ -17,6 +16,7 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) @@ -83,11 +83,13 @@ func generateJavaBuildFixture(t *testing.T, fixturePath string) { func TestParseJar(t *testing.T) { tests := []struct { + name string fixture string expected map[string]pkg.Package ignoreExtras []string }{ { + name: "example-jenkins-plugin", fixture: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi", ignoreExtras: []string{ "Plugin-Version", // has dynamic date @@ -146,6 +148,7 @@ func TestParseJar(t *testing.T) { }, }, { + name: "example-java-app-gradle", fixture: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar", expected: map[string]pkg.Package{ "example-java-app-gradle": { @@ -155,6 +158,15 @@ func TestParseJar(t *testing.T) { Language: pkg.Java, Type: pkg.JavaPkg, MetadataType: pkg.JavaMetadataType, + Licenses: pkg.NewLicenseSet( + pkg.License{ + Value: "Apache-2.0", + SPDXExpression: "Apache-2.0", + Type: license.Concluded, + URLs: internal.NewStringSet(), + Locations: file.NewLocationSet(file.NewLocation("test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar")), + }, + ), Metadata: pkg.JavaMetadata{ VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar", Manifest: &pkg.JavaManifest{ @@ -172,10 +184,20 @@ func TestParseJar(t *testing.T) { Language: pkg.Java, Type: pkg.JavaPkg, MetadataType: pkg.JavaMetadataType, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromFields( + "Apache 2", + "http://www.apache.org/licenses/LICENSE-2.0.txt", + func() *file.Location { + l := file.NewLocation("test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar") + return &l + }(), + ), + ), Metadata: pkg.JavaMetadata{ // ensure that nested packages with different names than that of the parent are appended as - // a suffix on the virtual path - VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar:joda-time", + // a suffix on the virtual path with a colon separator between group name and artifact name + VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar:joda-time:joda-time", PomProperties: &pkg.PomProperties{ Path: "META-INF/maven/joda-time/joda-time/pom.properties", GroupID: "joda-time", @@ -196,6 +218,7 @@ func TestParseJar(t *testing.T) { }, }, { + name: "example-java-app-maven", fixture: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar", ignoreExtras: []string{ "Build-Jdk", // can't guarantee the JDK used at build time @@ -209,6 +232,15 @@ func TestParseJar(t *testing.T) { Language: pkg.Java, Type: pkg.JavaPkg, MetadataType: pkg.JavaMetadataType, + Licenses: pkg.NewLicenseSet( + pkg.License{ + Value: "Apache-2.0", + SPDXExpression: "Apache-2.0", + Type: license.Concluded, + URLs: internal.NewStringSet(), + Locations: file.NewLocationSet(file.NewLocation("test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar")), + }, + ), Metadata: pkg.JavaMetadata{ VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar", Manifest: &pkg.JavaManifest{ @@ -231,16 +263,26 @@ func TestParseJar(t *testing.T) { }, }, "joda-time": { - Name: "joda-time", - Version: "2.9.2", - PURL: "pkg:maven/joda-time/joda-time@2.9.2", + Name: "joda-time", + Version: "2.9.2", + PURL: "pkg:maven/joda-time/joda-time@2.9.2", + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromFields( + "Apache 2", + "http://www.apache.org/licenses/LICENSE-2.0.txt", + func() *file.Location { + l := file.NewLocation("test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar") + return &l + }(), + ), + ), Language: pkg.Java, Type: pkg.JavaPkg, MetadataType: pkg.JavaMetadataType, Metadata: pkg.JavaMetadata{ // ensure that nested packages with different names than that of the parent are appended as // a suffix on the virtual path - VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar:joda-time", + VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar:joda-time:joda-time", PomProperties: &pkg.PomProperties{ Path: "META-INF/maven/joda-time/joda-time/pom.properties", GroupID: "joda-time", @@ -263,7 +305,7 @@ func TestParseJar(t *testing.T) { } for _, test := range tests { - t.Run(path.Base(test.fixture), func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { generateJavaBuildFixture(t, test.fixture) @@ -618,7 +660,7 @@ func Test_newPackageFromMavenData(t *testing.T) { tests := []struct { name string props pkg.PomProperties - project *pkg.PomProject + project *parsedPomProject parent *pkg.Package expectedParent pkg.Package expectedPackage *pkg.Package @@ -659,7 +701,7 @@ func Test_newPackageFromMavenData(t *testing.T) { Type: pkg.JavaPkg, MetadataType: pkg.JavaMetadataType, Metadata: pkg.JavaMetadata{ - VirtualPath: virtualPath + ":" + "some-artifact-id", + VirtualPath: virtualPath + ":" + "some-group-id" + ":" + "some-artifact-id", PomProperties: &pkg.PomProperties{ Name: "some-name", GroupID: "some-group-id", @@ -687,18 +729,29 @@ func Test_newPackageFromMavenData(t *testing.T) { ArtifactID: "some-artifact-id", Version: "1.0", }, - project: &pkg.PomProject{ - Parent: &pkg.PomParent{ - GroupID: "some-parent-group-id", - ArtifactID: "some-parent-artifact-id", - Version: "1.0-parent", + project: &parsedPomProject{ + PomProject: &pkg.PomProject{ + Parent: &pkg.PomParent{ + GroupID: "some-parent-group-id", + ArtifactID: "some-parent-artifact-id", + Version: "1.0-parent", + }, + Name: "some-name", + GroupID: "some-group-id", + ArtifactID: "some-artifact-id", + Version: "1.0", + Description: "desc", + URL: "aweso.me", + }, + Licenses: []pkg.License{ + { + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Declared, + URLs: internal.NewStringSet("https://opensource.org/licenses/MIT"), + Locations: file.NewLocationSet(file.NewLocation("some-license-path")), + }, }, - Name: "some-name", - GroupID: "some-group-id", - ArtifactID: "some-artifact-id", - Version: "1.0", - Description: "desc", - URL: "aweso.me", }, parent: &pkg.Package{ Name: "some-parent-name", @@ -727,8 +780,17 @@ func Test_newPackageFromMavenData(t *testing.T) { Language: pkg.Java, Type: pkg.JavaPkg, MetadataType: pkg.JavaMetadataType, + Licenses: pkg.NewLicenseSet( + pkg.License{ + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Declared, + URLs: internal.NewStringSet("https://opensource.org/licenses/MIT"), + Locations: file.NewLocationSet(file.NewLocation("some-license-path")), + }, + ), Metadata: pkg.JavaMetadata{ - VirtualPath: virtualPath + ":" + "some-artifact-id", + VirtualPath: virtualPath + ":" + "some-group-id" + ":" + "some-artifact-id", PomProperties: &pkg.PomProperties{ Name: "some-name", GroupID: "some-group-id", @@ -797,7 +859,7 @@ func Test_newPackageFromMavenData(t *testing.T) { Type: pkg.JenkinsPluginPkg, MetadataType: pkg.JavaMetadataType, Metadata: pkg.JavaMetadata{ - VirtualPath: virtualPath + ":" + "some-artifact-id", + VirtualPath: virtualPath + ":" + "com.cloudbees.jenkins.plugins" + ":" + "some-artifact-id", PomProperties: &pkg.PomProperties{ Name: "some-name", GroupID: "com.cloudbees.jenkins.plugins", @@ -894,44 +956,6 @@ func Test_newPackageFromMavenData(t *testing.T) { }, expectedPackage: nil, }, - { - name: "child matches parent by virtual path -- override name and version", - props: pkg.PomProperties{ - Name: "some-name", - GroupID: "some-group-id", - ArtifactID: "some-parent-name", // note: DOES NOT match parent package - Version: "3.0", // note: DOES NOT match parent package - }, - parent: &pkg.Package{ - Name: "", // note: empty, so should not be matched on - Version: "", // note: empty, so should not be matched on - Type: pkg.JavaPkg, - Metadata: pkg.JavaMetadata{ - VirtualPath: virtualPath, // note: matching virtual path - Manifest: nil, - PomProperties: nil, - Parent: nil, - }, - }, - expectedParent: pkg.Package{ - Name: "some-parent-name", - Version: "3.0", - Type: pkg.JavaPkg, - Metadata: pkg.JavaMetadata{ - VirtualPath: virtualPath, - Manifest: nil, - // note: we attach the discovered pom properties data - PomProperties: &pkg.PomProperties{ - Name: "some-name", - GroupID: "some-group-id", - ArtifactID: "some-parent-name", - Version: "3.0", - }, - Parent: nil, - }, - }, - expectedPackage: nil, - }, { name: "child matches parent by artifact id", props: pkg.PomProperties{ diff --git a/syft/pkg/cataloger/java/package_url.go b/syft/pkg/cataloger/java/package_url.go index b091ac383..df1baf791 100644 --- a/syft/pkg/cataloger/java/package_url.go +++ b/syft/pkg/cataloger/java/package_url.go @@ -1,6 +1,8 @@ package java import ( + "strings" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" @@ -9,9 +11,9 @@ import ( // PackageURL returns the PURL for the specific java package (see https://github.com/package-url/purl-spec) func packageURL(name, version string, metadata pkg.JavaMetadata) string { var groupID = name - groupIDs := cpe.GroupIDsFromJavaMetadata(metadata) - if len(groupIDs) > 0 { - groupID = groupIDs[0] + + if gID := groupIDFromJavaMetadata(name, metadata); gID != "" { + groupID = gID } pURL := packageurl.NewPackageURL( @@ -23,3 +25,121 @@ func packageURL(name, version string, metadata pkg.JavaMetadata) string { "") return pURL.ToString() } + +// GroupIDFromJavaPackage returns the authoritative group ID for a Java package. +// The order of precedence is: +// 1. The group ID from the POM properties +// 2. The group ID from the POM project +// 3. The group ID from a select map of known group IDs +// 4. The group ID from the Java manifest +func groupIDFromJavaMetadata(pkgName string, metadata pkg.JavaMetadata) (groupID string) { + if groupID = groupIDFromPomProperties(metadata.PomProperties); groupID != "" { + return groupID + } + + if groupID = groupIDFromPomProject(metadata.PomProject); groupID != "" { + return groupID + } + + if groupID = groupIDFromKnownPackageList(pkgName); groupID != "" { + return groupID + } + + if groupID = groupIDFromJavaManifest(metadata.Manifest); groupID != "" { + return groupID + } + + return groupID +} + +func groupIDFromKnownPackageList(pkgName string) (groupID string) { + if groupID, ok := cpe.DefaultArtifactIDToGroupID[pkgName]; ok { + return groupID + } + return groupID +} + +func groupIDFromJavaManifest(manifest *pkg.JavaManifest) (groupID string) { + if manifest == nil { + return groupID + } + + groupIDS := cpe.GetManifestFieldGroupIDs(manifest, cpe.PrimaryJavaManifestGroupIDFields) + // assumes that primaryJavaManifestNameFields are ordered by priority + if len(groupIDS) != 0 { + return groupIDS[0] + } + + groupIDS = cpe.GetManifestFieldGroupIDs(manifest, cpe.SecondaryJavaManifestGroupIDFields) + + if len(groupIDS) != 0 { + return groupIDS[0] + } + + return groupID +} + +func groupIDFromPomProperties(properties *pkg.PomProperties) (groupID string) { + if properties == nil { + return groupID + } + + if properties.GroupID != "" { + return cleanGroupID(properties.GroupID) + } + + // sometimes the publisher puts the group ID in the artifact ID field unintentionally + if looksLikeGroupID(properties.ArtifactID) { + // there is a strong indication that the artifact ID is really a group ID + return cleanGroupID(properties.ArtifactID) + } + + return groupID +} + +func groupIDFromPomProject(project *pkg.PomProject) (groupID string) { + if project == nil { + return groupID + } + + // check the project details + if project.GroupID != "" { + return cleanGroupID(project.GroupID) + } + + // sometimes the publisher puts the group ID in the artifact ID field unintentionally + if looksLikeGroupID(project.ArtifactID) { + // there is a strong indication that the artifact ID is really a group ID + return cleanGroupID(project.ArtifactID) + } + + // let's check the parent details + // if the current project does not have a group ID, but the parent does, we'll use the parent's group ID + if project.Parent != nil { + if project.Parent.GroupID != "" { + return cleanGroupID(project.Parent.GroupID) + } + + // sometimes the publisher puts the group ID in the artifact ID field unintentionally + if looksLikeGroupID(project.Parent.ArtifactID) { + // there is a strong indication that the artifact ID is really a group ID + return cleanGroupID(project.Parent.ArtifactID) + } + } + + return groupID +} +func looksLikeGroupID(value string) bool { + return strings.Contains(value, ".") +} + +func cleanGroupID(groupID string) string { + return strings.TrimSpace(removeOSCIDirectives(groupID)) +} + +func removeOSCIDirectives(groupID string) string { + // for example: + // org.bar;uses:=“org.foo” -> org.bar + // more about OSGI directives see https://spring.io/blog/2008/10/20/understanding-the-osgi-uses-directive/ + return strings.Split(groupID, ";")[0] +} diff --git a/syft/pkg/cataloger/java/package_url_test.go b/syft/pkg/cataloger/java/package_url_test.go index ac5eef6a0..7ffb0a365 100644 --- a/syft/pkg/cataloger/java/package_url_test.go +++ b/syft/pkg/cataloger/java/package_url_test.go @@ -10,10 +10,12 @@ import ( func Test_packageURL(t *testing.T) { tests := []struct { + name string pkg pkg.Package expect string }{ { + name: "maven", pkg: pkg.Package{ Name: "example-java-app-maven", Version: "0.1.0", @@ -38,6 +40,90 @@ func Test_packageURL(t *testing.T) { }, expect: "pkg:maven/org.anchore/example-java-app-maven@0.1.0", }, + { + name: "POM properties have explicit group ID without . in it", + pkg: pkg.Package{ + Name: "example-java-app-maven", + Version: "0.1.0", + Language: pkg.Java, + Type: pkg.JavaPkg, + MetadataType: pkg.JavaMetadataType, + Metadata: pkg.JavaMetadata{ + VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar", + Manifest: &pkg.JavaManifest{ + Main: map[string]string{ + "Manifest-Version": "1.0", + }, + }, + PomProperties: &pkg.PomProperties{ + Path: "META-INF/maven/org.anchore/example-java-app-maven/pom.properties", + GroupID: "commons", + ArtifactID: "example-java-app-maven", + Version: "0.1.0", + Extra: make(map[string]string), + }, + }, + }, + expect: "pkg:maven/commons/example-java-app-maven@0.1.0", + }, + { + name: "POM project has explicit group ID without . in it", + pkg: pkg.Package{ + Name: "example-java-app-maven", + Version: "0.1.0", + Language: pkg.Java, + Type: pkg.JavaPkg, + MetadataType: pkg.JavaMetadataType, + Metadata: pkg.JavaMetadata{ + VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar", + Manifest: &pkg.JavaManifest{ + Main: map[string]string{ + "Manifest-Version": "1.0", + }, + }, + PomProperties: &pkg.PomProperties{ + Path: "META-INF/maven/org.anchore/example-java-app-maven/pom.properties", + ArtifactID: "example-java-app-maven", + Version: "0.1.0", + Extra: make(map[string]string), + }, + PomProject: &pkg.PomProject{ + GroupID: "commons", + }, + }, + }, + expect: "pkg:maven/commons/example-java-app-maven@0.1.0", + }, + { + name: "POM project has explicit group ID without . in it", + pkg: pkg.Package{ + Name: "example-java-app-maven", + Version: "0.1.0", + Language: pkg.Java, + Type: pkg.JavaPkg, + MetadataType: pkg.JavaMetadataType, + Metadata: pkg.JavaMetadata{ + VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar", + Manifest: &pkg.JavaManifest{ + Main: map[string]string{ + "Manifest-Version": "1.0", + }, + }, + PomProperties: &pkg.PomProperties{ + Path: "META-INF/maven/org.anchore/example-java-app-maven/pom.properties", + ArtifactID: "example-java-app-maven", + Version: "0.1.0", + Extra: make(map[string]string), + }, + PomProject: &pkg.PomProject{ + Parent: &pkg.PomParent{ + GroupID: "parent", + }, + }, + }, + }, + expect: "pkg:maven/parent/example-java-app-maven@0.1.0", + }, } for _, tt := range tests { t.Run(tt.expect, func(t *testing.T) { @@ -45,3 +131,59 @@ func Test_packageURL(t *testing.T) { }) } } + +func Test_groupIDFromJavaMetadata(t *testing.T) { + tests := []struct { + name string + pkgName string + metadata pkg.JavaMetadata + expect string + }{ + { + name: "pom properties", + metadata: pkg.JavaMetadata{ + PomProperties: &pkg.PomProperties{ + GroupID: "org.anchore", + }, + }, + expect: "org.anchore", + }, + { + name: "pom project", + metadata: pkg.JavaMetadata{ + PomProject: &pkg.PomProject{ + GroupID: "org.anchore", + }, + }, + expect: "org.anchore", + }, + { + name: "known package list", + pkgName: "ant-antlr", + metadata: pkg.JavaMetadata{}, + expect: "org.apache.ant", + }, + { + name: "java manifest", + metadata: pkg.JavaMetadata{ + Manifest: &pkg.JavaManifest{ + Main: map[string]string{ + "Implementation-Vendor": "org.anchore", + }, + }, + }, + expect: "org.anchore", + }, + { + name: "no group id", + metadata: pkg.JavaMetadata{}, + expect: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expect, groupIDFromJavaMetadata(tt.pkgName, tt.metadata)) + }) + } +} diff --git a/syft/pkg/cataloger/java/parse_pom_xml.go b/syft/pkg/cataloger/java/parse_pom_xml.go index 704cb5598..eb0f9d9b2 100644 --- a/syft/pkg/cataloger/java/parse_pom_xml.go +++ b/syft/pkg/cataloger/java/parse_pom_xml.go @@ -1,6 +1,7 @@ package java import ( + "bytes" "encoding/xml" "fmt" "io" @@ -8,9 +9,11 @@ import ( "regexp" "strings" + "github.com/saintfish/chardet" "github.com/vifraa/gopom" "golang.org/x/net/html/charset" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" @@ -46,41 +49,65 @@ func parserPomXML(_ file.Resolver, _ *generic.Environment, reader file.LocationR return pkgs, nil, nil } -func parsePomXMLProject(path string, reader io.Reader) (*pkg.PomProject, error) { +func parsePomXMLProject(path string, reader io.Reader, location file.Location) (*parsedPomProject, error) { project, err := decodePomXML(reader) if err != nil { return nil, err } - return newPomProject(path, project), nil + return newPomProject(path, project, location), nil } -func newPomProject(path string, p gopom.Project) *pkg.PomProject { +func newPomProject(path string, p gopom.Project, location file.Location) *parsedPomProject { artifactID := safeString(p.ArtifactID) name := safeString(p.Name) projectURL := safeString(p.URL) - return &pkg.PomProject{ - Path: path, - Parent: pomParent(p, p.Parent), - GroupID: resolveProperty(p, p.GroupID), - ArtifactID: artifactID, - Version: resolveProperty(p, p.Version), - Name: name, - Description: cleanDescription(p.Description), - URL: projectURL, + + var licenses []pkg.License + if p.Licenses != nil { + for _, license := range *p.Licenses { + var licenseName, licenseURL string + if license.Name != nil { + licenseName = *license.Name + } + if license.URL != nil { + licenseURL = *license.URL + } + + if licenseName == "" && licenseURL == "" { + continue + } + + licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, licenseURL, &location)) + } + } + + log.WithFields("path", path, "artifactID", artifactID, "name", name, "projectURL", projectURL).Trace("parsing pom.xml") + return &parsedPomProject{ + PomProject: &pkg.PomProject{ + Path: path, + Parent: pomParent(p, p.Parent), + GroupID: resolveProperty(p, p.GroupID, "groupId"), + ArtifactID: artifactID, + Version: resolveProperty(p, p.Version, "version"), + Name: name, + Description: cleanDescription(p.Description), + URL: projectURL, + }, + Licenses: licenses, } } func newPackageFromPom(pom gopom.Project, dep gopom.Dependency, locations ...file.Location) pkg.Package { m := pkg.JavaMetadata{ PomProperties: &pkg.PomProperties{ - GroupID: resolveProperty(pom, dep.GroupID), - ArtifactID: resolveProperty(pom, dep.ArtifactID), - Scope: resolveProperty(pom, dep.Scope), + GroupID: resolveProperty(pom, dep.GroupID, "groupId"), + ArtifactID: resolveProperty(pom, dep.ArtifactID, "artifactId"), + Scope: resolveProperty(pom, dep.Scope, "scope"), }, } name := safeString(dep.ArtifactID) - version := resolveProperty(pom, dep.Version) + version := resolveProperty(pom, dep.Version, "version") p := pkg.Package{ Name: name, @@ -99,9 +126,15 @@ func newPackageFromPom(pom gopom.Project, dep gopom.Dependency, locations ...fil } func decodePomXML(content io.Reader) (project gopom.Project, err error) { - decoder := xml.NewDecoder(content) - // prevent against warnings for "xml: encoding "iso-8859-1" declared but Decoder.CharsetReader is nil" + inputReader, err := getUtf8Reader(content) + if err != nil { + return project, fmt.Errorf("unable to read pom.xml: %w", err) + } + + decoder := xml.NewDecoder(inputReader) + // when an xml file has a character set declaration (e.g. '') read that and use the correct decoder decoder.CharsetReader = charset.NewReaderLabel + if err := decoder.Decode(&project); err != nil { return project, fmt.Errorf("unable to unmarshal pom.xml: %w", err) } @@ -109,6 +142,33 @@ func decodePomXML(content io.Reader) (project gopom.Project, err error) { return project, nil } +func getUtf8Reader(content io.Reader) (io.Reader, error) { + pomContents, err := io.ReadAll(content) + if err != nil { + return nil, err + } + + detector := chardet.NewTextDetector() + detection, err := detector.DetectBest(pomContents) + + var inputReader io.Reader + if err == nil && detection != nil { + if detection.Charset == "UTF-8" { + inputReader = bytes.NewReader(pomContents) + } else { + inputReader, err = charset.NewReaderLabel(detection.Charset, bytes.NewReader(pomContents)) + if err != nil { + return nil, fmt.Errorf("unable to get encoding: %w", err) + } + } + } else { + // we could not detect the encoding, but we want a valid file to read. Replace unreadable + // characters with the UTF-8 replacement character. + inputReader = strings.NewReader(strings.ToValidUTF8(string(pomContents), "�")) + } + return inputReader, nil +} + func pomParent(pom gopom.Project, parent *gopom.Parent) (result *pkg.PomParent) { if parent == nil { return nil @@ -116,9 +176,9 @@ func pomParent(pom gopom.Project, parent *gopom.Parent) (result *pkg.PomParent) artifactID := safeString(parent.ArtifactID) result = &pkg.PomParent{ - GroupID: resolveProperty(pom, parent.GroupID), + GroupID: resolveProperty(pom, parent.GroupID, "groupId"), ArtifactID: artifactID, - Version: resolveProperty(pom, parent.Version), + Version: resolveProperty(pom, parent.Version, "version"), } if result.GroupID == "" && result.ArtifactID == "" && result.Version == "" { @@ -147,10 +207,11 @@ func cleanDescription(original *string) (cleaned string) { // If no match is found, the entire expression including ${} is returned // //nolint:gocognit -func resolveProperty(pom gopom.Project, property *string) string { +func resolveProperty(pom gopom.Project, property *string, propertyName string) string { propertyCase := safeString(property) + log.WithFields("existingPropertyValue", propertyCase, "propertyName", propertyName).Trace("resolving property") return propertyMatcher.ReplaceAllStringFunc(propertyCase, func(match string) string { - propertyName := strings.TrimSpace(match[2 : len(match)-1]) + propertyName := strings.TrimSpace(match[2 : len(match)-1]) // remove leading ${ and trailing } entries := pomProperties(pom) if value, ok := entries[propertyName]; ok { return value @@ -172,14 +233,26 @@ func resolveProperty(pom gopom.Project, property *string) string { for fieldNum := 0; fieldNum < pomValueType.NumField(); fieldNum++ { f := pomValueType.Field(fieldNum) tag := f.Tag.Get("xml") - tag = strings.TrimSuffix(tag, ",omitempty") + tag = strings.Split(tag, ",")[0] + // a segment of the property name matches the xml tag for the field, + // so we need to recurse down the nested structs or return a match + // if we're done. if part == tag { pomValue = pomValue.Field(fieldNum) pomValueType = pomValue.Type() if pomValueType.Kind() == reflect.Ptr { + // we were recursing down the nested structs, but one of the steps + // we need to take is a nil pointer, so give up and return the original match + if pomValue.IsNil() { + return match + } pomValue = pomValue.Elem() - pomValueType = pomValue.Type() + if !pomValue.IsZero() { + // we found a non-zero value whose tag matches this part of the property name + pomValueType = pomValue.Type() + } } + // If this was the last part of the property name, return the value if partNum == numParts-1 { return fmt.Sprintf("%v", pomValue.Interface()) } diff --git a/syft/pkg/cataloger/java/parse_pom_xml_test.go b/syft/pkg/cataloger/java/parse_pom_xml_test.go index edcf54144..acbadf883 100644 --- a/syft/pkg/cataloger/java/parse_pom_xml_test.go +++ b/syft/pkg/cataloger/java/parse_pom_xml_test.go @@ -1,13 +1,19 @@ package java import ( + "encoding/base64" + "io" "os" + "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/vifraa/gopom" + "github.com/anchore/syft/internal" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) @@ -63,6 +69,60 @@ func Test_parserPomXML(t *testing.T) { } } +func Test_decodePomXML_surviveNonUtf8Encoding(t *testing.T) { + // regression for https://github.com/anchore/syft/issues/2044 + + // we are storing the base64 contents of the pom.xml file. We are doing this to prevent accidental changes to the + // file, which is extremely important for this test. + + // for instance, even changing a single character in the file and saving in an IntelliJ IDE will automatically + // convert the file to UTF-8, which will break this test: + + // xxd with the original pom.xml + // 00000780: 6964 3e0d 0a20 2020 2020 2020 2020 2020 id>.. + // 00000790: 203c 6e61 6d65 3e4a e972 f46d 6520 4d69 J.r.me Mi + // 000007a0: 7263 3c2f 6e61 6d65 3e0d 0a20 2020 2020 rc.. + + // xxd with the pom.xml converted to UTF-8 (from a simple change with IntelliJ) + // 00000780: 6964 3e0d 0a20 2020 2020 2020 2020 2020 id>.. + // 00000790: 203c 6e61 6d65 3e4a efbf bd72 efbf bd6d J...r...m + // 000007a0: 6520 4d69 7263 3c2f 6e61 6d65 3e0d 0a20 e Mirc.. + + // Note that the name "Jérôme Mirc" was originally interpreted as "J.r.me Mi" and after the save + // is now encoded as "J...r...m" which is not what we want (note the extra bytes for each non UTF-8 character. + // The original 0xe9 byte (é) was converted to 0xefbfbd (�) which is the UTF-8 replacement character. + // This is quite silly on the part of IntelliJ, but it is what it is. + + cases := []struct { + name string + fixture string + }{ + { + name: "undeclared encoding", + fixture: "test-fixtures/pom/undeclared-iso-8859-encoded-pom.xml.base64", + }, + { + name: "declared encoding", + fixture: "test-fixtures/pom/declared-iso-8859-encoded-pom.xml.base64", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + fh, err := os.Open(c.fixture) + require.NoError(t, err) + + decoder := base64.NewDecoder(base64.StdEncoding, fh) + + proj, err := decodePomXML(decoder) + + require.NoError(t, err) + require.NotEmpty(t, proj.Developers) + }) + } + +} + func Test_parseCommonsTextPomXMLProject(t *testing.T) { tests := []struct { input string @@ -235,33 +295,79 @@ func Test_parseCommonsTextPomXMLProject(t *testing.T) { } func Test_parsePomXMLProject(t *testing.T) { + // TODO: ideally we would have the path to the contained pom.xml, not the jar + jarLocation := file.NewLocation("path/to/archive.jar") tests := []struct { - expected pkg.PomProject + name string + expected parsedPomProject }{ { - expected: pkg.PomProject{ - Path: "test-fixtures/pom/commons-codec.pom.xml", - Parent: &pkg.PomParent{ - GroupID: "org.apache.commons", - ArtifactID: "commons-parent", - Version: "42", + name: "go case", + expected: parsedPomProject{ + PomProject: &pkg.PomProject{ + Path: "test-fixtures/pom/commons-codec.pom.xml", + Parent: &pkg.PomParent{ + GroupID: "org.apache.commons", + ArtifactID: "commons-parent", + Version: "42", + }, + GroupID: "commons-codec", + ArtifactID: "commons-codec", + Version: "1.11", + Name: "Apache Commons Codec", + Description: "The Apache Commons Codec package contains simple encoder and decoders for various formats such as Base64 and Hexadecimal. In addition to these widely used encoders and decoders, the codec package also maintains a collection of phonetic encoding utilities.", + URL: "http://commons.apache.org/proper/commons-codec/", + }, + }, + }, + { + name: "with license data", + expected: parsedPomProject{ + PomProject: &pkg.PomProject{ + Path: "test-fixtures/pom/neo4j-license-maven-plugin.pom.xml", + Parent: &pkg.PomParent{ + GroupID: "org.sonatype.oss", + ArtifactID: "oss-parent", + Version: "7", + }, + GroupID: "org.neo4j.build.plugins", + ArtifactID: "license-maven-plugin", + Version: "4-SNAPSHOT", + Name: "${project.artifactId}", // TODO: this is not an ideal answer + Description: "Maven 2 plugin to check and update license headers in source files", + URL: "http://components.neo4j.org/${project.artifactId}/${project.version}", // TODO: this is not an ideal answer + }, + Licenses: []pkg.License{ + { + Value: "The Apache Software License, Version 2.0", + SPDXExpression: "", // TODO: ideally we would parse this title to get Apache-2.0 (created issue #2210 https://github.com/anchore/syft/issues/2210) + Type: license.Declared, + URLs: internal.NewStringSet("http://www.apache.org/licenses/LICENSE-2.0.txt"), + Locations: file.NewLocationSet(jarLocation), + }, + { + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Declared, + URLs: internal.NewStringSet(), + Locations: file.NewLocationSet(jarLocation), + }, + { + Type: license.Declared, + URLs: internal.NewStringSet("https://opensource.org/license/unlicense/"), + Locations: file.NewLocationSet(jarLocation), + }, }, - GroupID: "commons-codec", - ArtifactID: "commons-codec", - Version: "1.11", - Name: "Apache Commons Codec", - Description: "The Apache Commons Codec package contains simple encoder and decoders for various formats such as Base64 and Hexadecimal. In addition to these widely used encoders and decoders, the codec package also maintains a collection of phonetic encoding utilities.", - URL: "http://commons.apache.org/proper/commons-codec/", }, }, } for _, test := range tests { - t.Run(test.expected.Path, func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { fixture, err := os.Open(test.expected.Path) assert.NoError(t, err) - actual, err := parsePomXMLProject(fixture.Name(), fixture) + actual, err := parsePomXMLProject(fixture.Name(), fixture, jarLocation) assert.NoError(t, err) assert.Equal(t, &test.expected, actual) @@ -388,11 +494,29 @@ func Test_resolveProperty(t *testing.T) { }, expected: "org.some.parent", }, + { + name: "nil pointer halts search", + property: "${project.parent.groupId}", + pom: gopom.Project{ + Parent: nil, + }, + expected: "${project.parent.groupId}", + }, + { + name: "nil string pointer halts search", + property: "${project.parent.groupId}", + pom: gopom.Project{ + Parent: &gopom.Parent{ + GroupID: nil, + }, + }, + expected: "${project.parent.groupId}", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - resolved := resolveProperty(test.pom, stringPointer(test.property)) + resolved := resolveProperty(test.pom, stringPointer(test.property), test.name) assert.Equal(t, test.expected, resolved) }) } @@ -401,3 +525,28 @@ func Test_resolveProperty(t *testing.T) { func stringPointer(s string) *string { return &s } + +func Test_getUtf8Reader(t *testing.T) { + tests := []struct { + name string + contents string + }{ + { + name: "unknown encoding", + // random binary contents + contents: "BkiJz02JyEWE0nXR6TH///9NicpJweEETIucJIgAAABJicxPjQwhTY1JCE05WQh0BU2J0eunTYshTIusJIAAAAAPHwBNOeV1BUUx2+tWTIlUJDhMiUwkSEyJRCQgSIl8JFBMiQ==", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(tt.contents)) + + got, err := getUtf8Reader(decoder) + require.NoError(t, err) + gotBytes, err := io.ReadAll(got) + require.NoError(t, err) + // if we couldn't decode the section as UTF-8, we should get a replacement character + assert.Contains(t, string(gotBytes), "�") + }) + } +} diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/declared-iso-8859-encoded-pom.xml.base64 b/syft/pkg/cataloger/java/test-fixtures/pom/declared-iso-8859-encoded-pom.xml.base64 new file mode 100644 index 000000000..431baec1d --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/declared-iso-8859-encoded-pom.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iSVNPLTg4NTktMSI/PgoKPHByb2plY3QgeG1sbnM9Imh0dHA6Ly9tYXZlbi5hcGFjaGUub3JnL1BPTS80LjAuMCIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnNjaGVtYUxvY2F0aW9uPSJodHRwOi8vbWF2ZW4uYXBhY2hlLm9yZy9QT00vNC4wLjAgaHR0cDovL21hdmVuLmFwYWNoZS5vcmcvbWF2ZW4tdjRfMF8wLnhzZCI+DQogICAgPG1vZGVsVmVyc2lvbj40LjAuMDwvbW9kZWxWZXJzaW9uPg0KDQogICAgPG5hbWU+Q2FtZWxlb240SmF2YSAtIENvbnRlbnRTREs8L25hbWU+DQoNCiAgICA8Z3JvdXBJZD5jb20uYWxvZ2llbnQuY2FtZWxlb24uamF2YS5zZGs8L2dyb3VwSWQ+DQogICAgPGFydGlmYWN0SWQ+Y2FtZWxlb240amF2YS1zZGs8L2FydGlmYWN0SWQ+DQoNCiAgICA8dmVyc2lvbj4xLjEyLjI8L3ZlcnNpb24+DQoNCiAgICA8cGFja2FnaW5nPmphcjwvcGFja2FnaW5nPg0KDQogICAgPHNjbT4NCiAgICAgICAgPGNvbm5lY3Rpb24+c2NtOmdpdDpzc2g6Ly9naXRAZ2l0aHViLmNvbS9BbG9naWVudC9DT1M1LVNESy5naXQ8L2Nvbm5lY3Rpb24+DQogICAgICAgIDxkZXZlbG9wZXJDb25uZWN0aW9uPnNjbTpnaXQ6c3NoOi8vZ2l0QGdpdGh1Yi5jb20vQWxvZ2llbnQvQ09TNS1TREsuZ2l0PC9kZXZlbG9wZXJDb25uZWN0aW9uPg0KICAgICAgICA8dXJsPmdpdEBnaXRodWIuY29tOkFsb2dpZW50L0NPUzUtU0RLLmdpdDwvdXJsPg0KICAgIDwvc2NtPg0KDQogICAgPGRlc2NyaXB0aW9uPkNhbWVsZW9uIGlzIGEgQ01TIGRlZGljYXRlZCB0byBkZXZlbG9wZXJzIGFzIHdlbGwgYXMgbm9uLUlUIGNvbnRyaWJ1dG9ycy48L2Rlc2NyaXB0aW9uPg0KICAgIA0KICAgIDxvcmdhbml6YXRpb24+DQogICAgICAgIDxuYW1lPkFsb2dpZW50PC9uYW1lPg0KICAgICAgICA8dXJsPmh0dHA6Ly93d3cuYWxvZ2llbnQuY29tPC91cmw+DQogICAgPC9vcmdhbml6YXRpb24+DQoNCiAgICA8dXJsPmh0dHA6Ly93d3cuY2FtZWxlb25jbXMuY29tPC91cmw+DQoNCiAgICA8ZGV2ZWxvcGVycz4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5hbW9yaW48L2lkPg0KICAgICAgICAgICAgPG5hbWU+QXJuYXVkIE1vcmluPC9uYW1lPg0KICAgICAgICAgICAgPGVtYWlsPmFtb3JpbkBhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5hbG9yZDwvaWQ+DQogICAgICAgICAgICA8bmFtZT5BbGV4YW5kcmUgTG9yZDwvbmFtZT4NCiAgICAgICAgICAgIDxlbWFpbD5hbG9yZEBhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5kam9tcGhlPC9pZD4NCiAgICAgICAgICAgIDxuYW1lPkRhbmllbCBKb21waGU8L25hbWU+DQogICAgICAgICAgICA8ZW1haWw+ZGpvbXBoZUBhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5qbWlyYzwvaWQ+DQogICAgICAgICAgICA8bmFtZT5K6XL0bWUgTWlyYzwvbmFtZT4NCiAgICAgICAgICAgIDxlbWFpbD5qbWlyY0BhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5wbHZlaWxsZXV4PC9pZD4NCiAgICAgICAgICAgIDxuYW1lPlBpZXJyZS1MdWMgVmVpbGxldXg8L25hbWU+DQogICAgICAgICAgICA8ZW1haWw+cGx2ZWlsbGV1eEBhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICA8L2RldmVsb3BlcnM+DQoNCg0KICAgIDxsaWNlbnNlcz4NCiAgICAgICAgPGxpY2Vuc2U+DQogICAgICAgICAgICA8bmFtZT5UaGUgQXBhY2hlIFNvZnR3YXJlIExpY2Vuc2UsIFZlcnNpb24gMi4wPC9uYW1lPg0KICAgICAgICAgICAgPHVybD5odHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAudHh0PC91cmw+DQogICAgICAgICAgICA8ZGlzdHJpYnV0aW9uPnJlcG88L2Rpc3RyaWJ1dGlvbj4NCiAgICAgICAgPC9saWNlbnNlPg0KICAgIDwvbGljZW5zZXM+DQoNCiAgICA8YnVpbGQ+DQoNCiAgICAgICAgPHJlc291cmNlcz4NCiAgICAgICAgICAgIDxyZXNvdXJjZT4NCiAgICAgICAgICAgICAgICA8ZmlsdGVyaW5nPmZhbHNlPC9maWx0ZXJpbmc+DQogICAgICAgICAgICAgICAgPGRpcmVjdG9yeT5zcmMvbWFpbi9yZXNvdXJjZXM8L2RpcmVjdG9yeT4NCiAgICAgICAgICAgIDwvcmVzb3VyY2U+DQogICAgICAgICAgICA8cmVzb3VyY2U+DQogICAgICAgICAgICAgICAgPGZpbHRlcmluZz5mYWxzZTwvZmlsdGVyaW5nPg0KICAgICAgICAgICAgICAgIDxkaXJlY3Rvcnk+c3JjL21haW4vamF2YTwvZGlyZWN0b3J5Pg0KICAgICAgICAgICAgICAgIDxpbmNsdWRlcz4NCiAgICAgICAgICAgICAgICAgICAgPGluY2x1ZGU+Kio8L2luY2x1ZGU+DQogICAgICAgICAgICAgICAgPC9pbmNsdWRlcz4NCiAgICAgICAgICAgICAgICA8ZXhjbHVkZXM+DQogICAgICAgICAgICAgICAgICAgIDxleGNsdWRlPioqLyouamF2YTwvZXhjbHVkZT4NCiAgICAgICAgICAgICAgICA8L2V4Y2x1ZGVzPg0KICAgICAgICAgICAgPC9yZXNvdXJjZT4NCiAgICAgICAgPC9yZXNvdXJjZXM+DQogICAgICAgIDx0ZXN0UmVzb3VyY2VzPg0KICAgICAgICAgICAgPHRlc3RSZXNvdXJjZT4NCiAgICAgICAgICAgICAgICA8ZmlsdGVyaW5nPmZhbHNlPC9maWx0ZXJpbmc+DQogICAgICAgICAgICAgICAgPGRpcmVjdG9yeT5zcmMvdGVzdC9yZXNvdXJjZXM8L2RpcmVjdG9yeT4NCiAgICAgICAgICAgIDwvdGVzdFJlc291cmNlPg0KICAgICAgICAgICAgPHRlc3RSZXNvdXJjZT4NCiAgICAgICAgICAgICAgICA8ZmlsdGVyaW5nPmZhbHNlPC9maWx0ZXJpbmc+DQogICAgICAgICAgICAgICAgPGRpcmVjdG9yeT5zcmMvdGVzdC9qYXZhPC9kaXJlY3Rvcnk+DQogICAgICAgICAgICAgICAgPGluY2x1ZGVzPg0KICAgICAgICAgICAgICAgICAgICA8aW5jbHVkZT4qKjwvaW5jbHVkZT4NCiAgICAgICAgICAgICAgICA8L2luY2x1ZGVzPg0KICAgICAgICAgICAgICAgIDxleGNsdWRlcz4NCiAgICAgICAgICAgICAgICAgICAgPGV4Y2x1ZGU+KiovKi5qYXZhPC9leGNsdWRlPg0KICAgICAgICAgICAgICAgIDwvZXhjbHVkZXM+DQogICAgICAgICAgICA8L3Rlc3RSZXNvdXJjZT4NCiAgICAgICAgPC90ZXN0UmVzb3VyY2VzPg0KDQogICAgICAgIDxwbHVnaW5zPg0KICAgICAgICAgICAgPHBsdWdpbj4NCiAgICAgICAgICAgICAgICA8Z3JvdXBJZD5vcmcuYXBhY2hlLm1hdmVuLnBsdWdpbnM8L2dyb3VwSWQ+DQogICAgICAgICAgICAgICAgPGFydGlmYWN0SWQ+bWF2ZW4tY29tcGlsZXItcGx1Z2luPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgICAgIDx2ZXJzaW9uPjIuMy4yPC92ZXJzaW9uPg0KICAgICAgICAgICAgICAgIDxjb25maWd1cmF0aW9uPg0KICAgICAgICAgICAgICAgICAgICA8c291cmNlPjEuNjwvc291cmNlPg0KICAgICAgICAgICAgICAgICAgICA8dGFyZ2V0PjEuNjwvdGFyZ2V0Pg0KICAgICAgICAgICAgICAgIDwvY29uZmlndXJhdGlvbj4NCiAgICAgICAgICAgIDwvcGx1Z2luPg0KICAgICAgICAgICAgPHBsdWdpbj4NCiAgICAgICAgICAgICAgICA8Z3JvdXBJZD5vcmcuYXBhY2hlLm1hdmVuLnBsdWdpbnM8L2dyb3VwSWQ+DQogICAgICAgICAgICAgICAgPGFydGlmYWN0SWQ+bWF2ZW4tc291cmNlLXBsdWdpbjwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgICAgICA8dmVyc2lvbj4yLjEuMjwvdmVyc2lvbj4NCiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9ucz4NCiAgICAgICAgICAgICAgICAgICAgPGV4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxpZD5hdHRhY2gtc291cmNlczwvaWQ+DQogICAgICAgICAgICAgICAgICAgICAgICA8cGhhc2U+dmVyaWZ5PC9waGFzZT4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxnb2Fscz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Z29hbD5qYXI8L2dvYWw+DQogICAgICAgICAgICAgICAgICAgICAgICA8L2dvYWxzPg0KICAgICAgICAgICAgICAgICAgICA8L2V4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICA8L2V4ZWN1dGlvbnM+DQogICAgICAgICAgICA8L3BsdWdpbj4NCiAgICAgICAgICAgIDxwbHVnaW4+DQogICAgICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLmFwYWNoZS5tYXZlbi5wbHVnaW5zPC9ncm91cElkPg0KICAgICAgICAgICAgICAgIDxhcnRpZmFjdElkPm1hdmVuLWphdmFkb2MtcGx1Z2luPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgICAgIDx2ZXJzaW9uPjIuNzwvdmVyc2lvbj4NCiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9ucz4NCiAgICAgICAgICAgICAgICAgICAgPGV4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxpZD5hdHRhY2gtamF2YWRvY3M8L2lkPg0KICAgICAgICAgICAgICAgICAgICAgICAgPGdvYWxzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxnb2FsPmphcjwvZ29hbD4NCiAgICAgICAgICAgICAgICAgICAgICAgIDwvZ29hbHM+DQogICAgICAgICAgICAgICAgICAgIDwvZXhlY3V0aW9uPg0KICAgICAgICAgICAgICAgIDwvZXhlY3V0aW9ucz4NCiAgICAgICAgICAgIDwvcGx1Z2luPg0KICAgICAgICAgICAgPHBsdWdpbj4NCiAgICAgICAgICAgICAgICA8Z3JvdXBJZD5vcmcuYXBhY2hlLm1hdmVuLnBsdWdpbnM8L2dyb3VwSWQ+DQogICAgICAgICAgICAgICAgPGFydGlmYWN0SWQ+bWF2ZW4tcmVsZWFzZS1wbHVnaW48L2FydGlmYWN0SWQ+DQogICAgICAgICAgICAgICAgPHZlcnNpb24+Mi4xPC92ZXJzaW9uPg0KICAgICAgICAgICAgPC9wbHVnaW4+DQoJCQkJICAgIDxwbHVnaW4+DQogICAgICAJCQkJICA8Z3JvdXBJZD5vcmcuYXBhY2hlLm1hdmVuLnBsdWdpbnM8L2dyb3VwSWQ+DQogICAgICAgIAkJCQk8YXJ0aWZhY3RJZD5tYXZlbi1yZXBvc2l0b3J5LXBsdWdpbjwvYXJ0aWZhY3RJZD4NCiAgICAgICAgCQkJCTx2ZXJzaW9uPjIuMy4xPC92ZXJzaW9uPg0KICAgICAgCQkJPC9wbHVnaW4+DQogICAgICAgICAgICA8cGx1Z2luPg0KICAgICAgICAgICAgICAgIDxncm91cElkPm9yZy5hcGFjaGUubWF2ZW4ucGx1Z2luczwvZ3JvdXBJZD4NCiAgICAgICAgICAgICAgICA8YXJ0aWZhY3RJZD5tYXZlbi1ncGctcGx1Z2luPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgICAgIDx2ZXJzaW9uPjEuMTwvdmVyc2lvbj4NCiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9ucz4NCiAgICAgICAgICAgICAgICAgICAgPGV4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxpZD5zaWduLWFydGlmYWN0czwvaWQ+DQogICAgICAgICAgICAgICAgICAgICAgICA8cGhhc2U+dmVyaWZ5PC9waGFzZT4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxnb2Fscz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Z29hbD5zaWduPC9nb2FsPg0KICAgICAgICAgICAgICAgICAgICAgICAgPC9nb2Fscz4NCiAgICAgICAgICAgICAgICAgICAgPC9leGVjdXRpb24+DQogICAgICAgICAgICAgICAgPC9leGVjdXRpb25zPg0KICAgICAgICAgICAgPC9wbHVnaW4+DQogICAgICAJIDwvcGx1Z2lucz4NCiAgICA8L2J1aWxkPg0KDQogICAgPGRlcGVuZGVuY2llcz4NCg0KICAgICAgICA8IS0tIFNQUklORyBERVBFTkRFTkNJRVMgLS0+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1jb3JlPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+My4wLjQuUkVMRUFTRTwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxleGNsdXNpb25zPg0KICAgICAgICAgICAgICAgIDxleGNsdXNpb24+DQogICAgICAgICAgICAgICAgICAgIDxhcnRpZmFjdElkPmNvbW1vbnMtbG9nZ2luZzwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgICAgICAgICAgPGdyb3VwSWQ+Y29tbW9ucy1sb2dnaW5nPC9ncm91cElkPg0KICAgICAgICAgICAgICAgIDwvZXhjbHVzaW9uPg0KICAgICAgICAgICAgPC9leGNsdXNpb25zPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy13ZWI8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuNC5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1vcm08L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuNC5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1hc3BlY3RzPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+My4wLjQuUkVMRUFTRTwvdmVyc2lvbj4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy5zcHJpbmdmcmFtZXdvcms8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5zcHJpbmctZXhwcmVzc2lvbjwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjMuMC40LlJFTEVBU0U8L3ZlcnNpb24+DQogICAgICAgIDwvZGVwZW5kZW5jeT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5vcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5PC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+c3ByaW5nLXNlY3VyaXR5LWNvcmU8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuMy5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yay5zZWN1cml0eTwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1zZWN1cml0eS1jb25maWc8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuMy5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yay5zZWN1cml0eTwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1zZWN1cml0eS13ZWI8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuMy5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQoNCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5jb20uc3ByaW5nc291cmNlLmluc2lnaHQ8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5pbnNpZ2h0LWFubm90YXRpb248L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjAuMC5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICAgICAgPHNjb3BlPnByb3ZpZGVkPC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KDQogICAgICAgIDwhLS0gU0VSVkxFVCBERVBFTkRFTkNJRVMgLS0+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+amF2YXguc2VydmxldDwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNlcnZsZXQtYXBpPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+Mi41PC92ZXJzaW9uPg0KICAgICAgICAgICAgPHNjb3BlPnByb3ZpZGVkPC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KDQogICAgICAgIDwhLS0gTE9HR0lORyBERVBFTkRFTkNJRVMgLSBMT0c0SiAtLT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5jaC5xb3MubG9nYmFjazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPmxvZ2JhY2stY2xhc3NpYzwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjAuOS4yNjwvdmVyc2lvbj4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KDQogICAgICAgIDwhLS0gQVBBQ0hFIENPTU1PTlMgREVQRU5ERU5DSUVTIC0tPg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPmNvbW1vbnMtaW88L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5jb21tb25zLWlvPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+Mi4wPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+Y29tbW9ucy1sYW5nPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+Y29tbW9ucy1sYW5nPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+Mi41PC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLmFwYWNoZS5jb21tb25zPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+Y29tbW9ucy1lbWFpbDwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjEuMjwvdmVyc2lvbj4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPmNvbW1vbnMtYmVhbnV0aWxzPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+Y29tbW9ucy1iZWFudXRpbHM8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjguMzwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxleGNsdXNpb25zPg0KICAgICAgICAgICAgICAgIDxleGNsdXNpb24+DQogICAgICAgICAgICAgICAgICAgIDxhcnRpZmFjdElkPmNvbW1vbnMtbG9nZ2luZzwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgICAgICAgICAgPGdyb3VwSWQ+Y29tbW9ucy1sb2dnaW5nPC9ncm91cElkPg0KICAgICAgICAgICAgICAgIDwvZXhjbHVzaW9uPg0KICAgICAgICAgICAgPC9leGNsdXNpb25zPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+Y29tbW9ucy1jb2RlYzwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPmNvbW1vbnMtY29kZWM8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjQ8L3ZlcnNpb24+DQogICAgICAgIDwvZGVwZW5kZW5jeT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5jb21tb25zLWNvbmZpZ3VyYXRpb248L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5jb21tb25zLWNvbmZpZ3VyYXRpb248L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjY8L3ZlcnNpb24+DQogICAgICAgIDwvZGVwZW5kZW5jeT4NCg0KICAgICAgICA8IS0tIEhJQkVSTkFURSBERVBFTkRFTkNJRVMgLS0+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLmhpYmVybmF0ZTwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPmhpYmVybmF0ZS1jb3JlPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+My42LjAuRmluYWw8L3ZlcnNpb24+DQogICAgICAgICAgICA8ZXhjbHVzaW9ucz4NCiAgICAgICAgICAgICAgICA8ZXhjbHVzaW9uPg0KICAgICAgICAgICAgICAgICAgICA8YXJ0aWZhY3RJZD5zbGY0ai1hcGk8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICAgICAgICAgIDxncm91cElkPm9yZy5zbGY0ajwvZ3JvdXBJZD4NCiAgICAgICAgICAgICAgICA8L2V4Y2x1c2lvbj4NCiAgICAgICAgICAgICAgICA8ZXhjbHVzaW9uPg0KICAgICAgICAgICAgICAgICAgICA8YXJ0aWZhY3RJZD5jb21tb25zLWNvbGxlY3Rpb25zPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgICAgICAgICA8Z3JvdXBJZD5jb21tb25zLWNvbGxlY3Rpb25zPC9ncm91cElkPg0KICAgICAgICAgICAgICAgIDwvZXhjbHVzaW9uPg0KICAgICAgICAgICAgPC9leGNsdXNpb25zPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+amF2YXNzaXN0PC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+amF2YXNzaXN0PC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+My4xMS4wLkdBPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+aHNxbGRiPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+aHNxbGRiPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+MS44LjAuMTA8L3ZlcnNpb24+DQogICAgICAgICAgICA8c2NvcGU+dGVzdDwvc2NvcGU+DQogICAgICAgIDwvZGVwZW5kZW5jeT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5jb20ubWljcm9zb2Z0PC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+c3FsamRiYzwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjIuMDwvdmVyc2lvbj4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KDQogICAgICAgIDwhLS0gVU5JVCBURVNUSU5HIC0tPg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy51bml0aWxzPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+dW5pdGlsczwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjIuNDwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxzY29wZT50ZXN0PC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPmp1bml0PC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+anVuaXQ8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj40LjguMjwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxzY29wZT50ZXN0PC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy5kYnVuaXQ8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5kYnVuaXQ8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4yLjQuODwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxzY29wZT50ZXN0PC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy5tb2NraXRvPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+bW9ja2l0by1hbGw8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjguNTwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxzY29wZT50ZXN0PC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPnhtbHVuaXQ8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD54bWx1bml0PC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+MS4zPC92ZXJzaW9uPg0KICAgICAgICAgICAgPHNjb3BlPnRlc3Q8L3Njb3BlPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQoNCiAgICAgICAgPCEtLSBPVEhFUiAtLT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5vcmcuYXBhY2hlLnNvbHI8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5zb2xyLXNvbHJqPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+MS40LjE8L3ZlcnNpb24+DQogICAgICAgICAgICA8ZXhjbHVzaW9ucz4NCiAgICAgICAgICAgICAgICA8ZXhjbHVzaW9uPg0KICAgICAgICAgICAgICAgICAgICA8YXJ0aWZhY3RJZD5zbGY0ai1hcGk8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICAgICAgICAgIDxncm91cElkPm9yZy5zbGY0ajwvZ3JvdXBJZD4NCiAgICAgICAgICAgICAgICA8L2V4Y2x1c2lvbj4NCiAgICAgICAgICAgIDwvZXhjbHVzaW9ucz4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy5jb3MuY29tbW9uPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+Y29zLXZhbGlkYXRvcjwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjEuMC4wPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+Y29tLmdvb2dsZS5ndWF2YTwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPmd1YXZhPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+cjA3PC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQoNCiAgICA8L2RlcGVuZGVuY2llcz4NCg0KICAgIDxwcm9maWxlcz4NCiAgICAgICAgPHByb2ZpbGU+DQogICAgICAgICAgICA8aWQ+anJlYmVsPC9pZD4NCiAgICAgICAgICAgIDxhY3RpdmF0aW9uPg0KICAgICAgICAgICAgICAgIDxhY3RpdmVCeURlZmF1bHQ+ZmFsc2U8L2FjdGl2ZUJ5RGVmYXVsdD4NCiAgICAgICAgICAgIDwvYWN0aXZhdGlvbj4NCiAgICAgICAgICAgIDxidWlsZD4NCiAgICAgICAgICAgICAgICA8cGx1Z2lucz4NCiAgICAgICAgICAgICAgICAgICAgPHBsdWdpbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxncm91cElkPm9yZy56ZXJvdHVybmFyb3VuZDwvZ3JvdXBJZD4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxhcnRpZmFjdElkPmphdmFyZWJlbC1tYXZlbi1wbHVnaW48L2FydGlmYWN0SWQ+DQogICAgICAgICAgICAgICAgICAgICAgICA8dmVyc2lvbj4xLjAuNTwvdmVyc2lvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxleGVjdXRpb25zPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxleGVjdXRpb24+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpZD5nZW5lcmF0ZS1yZWJlbC14bWw8L2lkPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGhhc2U+cHJvY2Vzcy1yZXNvdXJjZXM8L3BoYXNlPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Z29hbHM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Z29hbD5nZW5lcmF0ZTwvZ29hbD4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9nb2Fscz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2V4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDwvZXhlY3V0aW9ucz4NCiAgICAgICAgICAgICAgICAgICAgPC9wbHVnaW4+DQogICAgICAgICAgICAgICAgPC9wbHVnaW5zPg0KICAgICAgICAgICAgPC9idWlsZD4NCiAgICAgICAgICAgIDxwcm9wZXJ0aWVzPg0KICAgICAgICAgICAgICAgIDxqZXR0eS1zY2FuSW50ZXJ2YWxTZWNvbmRzPjA8L2pldHR5LXNjYW5JbnRlcnZhbFNlY29uZHM+DQogICAgICAgICAgICA8L3Byb3BlcnRpZXM+DQogICAgICAgIDwvcHJvZmlsZT4NCiAgICA8L3Byb2ZpbGVzPg0KPC9wcm9qZWN0Pg0K diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/neo4j-license-maven-plugin.pom.xml b/syft/pkg/cataloger/java/test-fixtures/pom/neo4j-license-maven-plugin.pom.xml new file mode 100644 index 000000000..dc8a072ad --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/neo4j-license-maven-plugin.pom.xml @@ -0,0 +1,459 @@ + + + + 4.0.0 + + org.sonatype.oss + oss-parent + 7 + + + + org.neo4j.build.plugins + license-maven-pluginå + 4-SNAPSHOT + maven-plugin + + ${project.artifactId} + Maven 2 plugin to check and update license headers in source files + http://components.neo4j.org/${project.artifactId}/${project.version} + 2008 + + + + + 1.6 + 1.6 + + + + + + scm:git:git://github.com/neo4j/license-maven-plugin.git + scm:git:git@github.com:neo4j/license-maven-plugin.git + https://github.com/neo4j/license-maven-plugin + + + + + + Mathieu Carbou + http://mathieu.carbou.free.fr/ + + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + MIT + + + https://opensource.org/license/unlicense/ + + + + + + mimilowns + Cédric + mimilowns@gmail.com + +1 + + developer + + + + mathieu.carbou + Mathieu Carbou + mathieu.carbou@gmail.com + Mycila + http://mathieu.carbou.free.fr/ + -5 + + project owner + developer + + + + + + Google Code + http://code.google.com/p/${project.artifactId}/issues/list + + + + + + + maven-license-plugin-announce + maven-license-plugin-announce-subscribe@googlegroups.com + maven-license-plugin-announce-unsubscribe@googlegroups.com + http://groups.google.com/group/maven-license-plugin-announce + + + maven-license-plugin-codesite + maven-license-plugin-codesite-subscribe@googlegroups.com + maven-license-plugin-codesite-unsubscribe@googlegroups.com + http://groups.google.com/group/maven-license-plugin-codesite + + + + + + + + sonatype-nexus-staging + Nexus Release Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + snapshots@m2.neo4j.org + snapshots@m2.neo4j.org + http://m2.neo4j.org/content/repositories/snapshots + + + neo4j-site + scpexe://components.neo4j.org/home/neo/components/${project.artifactId}/${project.version} + + + + + + + + + org.apache.maven.wagon + wagon-webdav + 1.0-beta-2 + + + + + + org.apache.felix + maven-bundle-plugin + 2.1.0 + + + org.apache.felix + maven-ipojo-plugin + 1.6.0 + + + com.mycila.maven-license-plugin + maven-license-plugin + 1.8.0 + + + maven-antrun-plugin + 1.4 + + + maven-source-plugin + 2.1.2 + + + maven-jar-plugin + 2.3.1 + + + maven-javadoc-plugin + 2.7 + + + maven-deploy-plugin + 2.5 + + + maven-clean-plugin + 2.4.1 + + + maven-invoker-plugin + 1.5 + + + maven-shade-plugin + 1.3.3 + + + maven-assembly-plugin + 2.2-beta-5 + + + maven-dependency-plugin + 2.1 + + + maven-compiler-plugin + 2.3.1 + + + maven-release-plugin + 2.0 + + + maven-surefire-plugin + 2.6 + + + org.apache.maven.plugins + maven-gpg-plugin + 1.4 + + Neo Technology Build Server + true + + + + + + + + maven-plugin-plugin + 2.6 + + + maven-clean-plugin + + + + .clover + + + test-output + + + + + + + maven-compiler-plugin + + ${jdk} + ${jdk} + + + + + maven-surefire-plugin + + + + maven-jar-plugin + + + + true + true + + + + + + maven-source-plugin + + + attach-sources + + jar + + + + + + maven-remote-resources-plugin + 1.1 + + + + process + + + + org.apache:apache-jar-resource-bundle:1.3 + + + + + + + maven-assembly-plugin + + + project + + + + + + maven-deploy-plugin + + true + + + + maven-release-plugin + + + + maven-dependency-plugin + + + com.mycila.maven-license-plugin + maven-license-plugin + 1.9.0 + +
${basedir}/src/etc/header.txt
+ true + + .gitignore + LICENSE.txt + NOTICE.txt + src/test/data/** + src/test/integration/** + src/test/resources/** + +
+ + + + check + + + +
+ + + maven-clean-plugin + + + + it + + target/** + */target/** + + + + target + + + + + + true + maven-invoker-plugin + + it + true + true + ${ittest.skip} + ${ittest.skip} + + ${target.version} + + + test + + + **/pom.xml + + + + + integration-test + + install + run + + + + +
+
+ + + + + + org.apache.maven + maven-plugin-api + 3.0.1 + compile + + + com.mycila.xmltool + xmltool + 3.3 + compile + + + org.apache.maven + maven-project + 3.0-alpha-2 + compile + + + junit + junit + + + + + org.codehaus.plexus + plexus-utils + 2.0.5 + compile + + + + org.testng + testng + 5.7 + jdk15 + test + + + junit + junit + + + + + org.apache.maven + maven-embedder + 3.0.1 + test + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + 2.0-alpha-1 + test + + + junit + junit + + + + + +
diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/undeclared-iso-8859-encoded-pom.xml.base64 b/syft/pkg/cataloger/java/test-fixtures/pom/undeclared-iso-8859-encoded-pom.xml.base64 new file mode 100644 index 000000000..6b8c3ce71 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/undeclared-iso-8859-encoded-pom.xml.base64 @@ -0,0 +1 @@ +PHByb2plY3QgeG1sbnM9Imh0dHA6Ly9tYXZlbi5hcGFjaGUub3JnL1BPTS80LjAuMCIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnNjaGVtYUxvY2F0aW9uPSJodHRwOi8vbWF2ZW4uYXBhY2hlLm9yZy9QT00vNC4wLjAgaHR0cDovL21hdmVuLmFwYWNoZS5vcmcvbWF2ZW4tdjRfMF8wLnhzZCI+DQogICAgPG1vZGVsVmVyc2lvbj40LjAuMDwvbW9kZWxWZXJzaW9uPg0KDQogICAgPG5hbWU+Q2FtZWxlb240SmF2YSAtIENvbnRlbnRTREs8L25hbWU+DQoNCiAgICA8Z3JvdXBJZD5jb20uYWxvZ2llbnQuY2FtZWxlb24uamF2YS5zZGs8L2dyb3VwSWQ+DQogICAgPGFydGlmYWN0SWQ+Y2FtZWxlb240amF2YS1zZGs8L2FydGlmYWN0SWQ+DQoNCiAgICA8dmVyc2lvbj4xLjEyLjI8L3ZlcnNpb24+DQoNCiAgICA8cGFja2FnaW5nPmphcjwvcGFja2FnaW5nPg0KDQogICAgPHNjbT4NCiAgICAgICAgPGNvbm5lY3Rpb24+c2NtOmdpdDpzc2g6Ly9naXRAZ2l0aHViLmNvbS9BbG9naWVudC9DT1M1LVNESy5naXQ8L2Nvbm5lY3Rpb24+DQogICAgICAgIDxkZXZlbG9wZXJDb25uZWN0aW9uPnNjbTpnaXQ6c3NoOi8vZ2l0QGdpdGh1Yi5jb20vQWxvZ2llbnQvQ09TNS1TREsuZ2l0PC9kZXZlbG9wZXJDb25uZWN0aW9uPg0KICAgICAgICA8dXJsPmdpdEBnaXRodWIuY29tOkFsb2dpZW50L0NPUzUtU0RLLmdpdDwvdXJsPg0KICAgIDwvc2NtPg0KDQogICAgPGRlc2NyaXB0aW9uPkNhbWVsZW9uIGlzIGEgQ01TIGRlZGljYXRlZCB0byBkZXZlbG9wZXJzIGFzIHdlbGwgYXMgbm9uLUlUIGNvbnRyaWJ1dG9ycy48L2Rlc2NyaXB0aW9uPg0KICAgIA0KICAgIDxvcmdhbml6YXRpb24+DQogICAgICAgIDxuYW1lPkFsb2dpZW50PC9uYW1lPg0KICAgICAgICA8dXJsPmh0dHA6Ly93d3cuYWxvZ2llbnQuY29tPC91cmw+DQogICAgPC9vcmdhbml6YXRpb24+DQoNCiAgICA8dXJsPmh0dHA6Ly93d3cuY2FtZWxlb25jbXMuY29tPC91cmw+DQoNCiAgICA8ZGV2ZWxvcGVycz4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5hbW9yaW48L2lkPg0KICAgICAgICAgICAgPG5hbWU+QXJuYXVkIE1vcmluPC9uYW1lPg0KICAgICAgICAgICAgPGVtYWlsPmFtb3JpbkBhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5hbG9yZDwvaWQ+DQogICAgICAgICAgICA8bmFtZT5BbGV4YW5kcmUgTG9yZDwvbmFtZT4NCiAgICAgICAgICAgIDxlbWFpbD5hbG9yZEBhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5kam9tcGhlPC9pZD4NCiAgICAgICAgICAgIDxuYW1lPkRhbmllbCBKb21waGU8L25hbWU+DQogICAgICAgICAgICA8ZW1haWw+ZGpvbXBoZUBhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5qbWlyYzwvaWQ+DQogICAgICAgICAgICA8bmFtZT5K6XL0bWUgTWlyYzwvbmFtZT4NCiAgICAgICAgICAgIDxlbWFpbD5qbWlyY0BhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICAgICAgPGRldmVsb3Blcj4NCiAgICAgICAgICAgIDxpZD5wbHZlaWxsZXV4PC9pZD4NCiAgICAgICAgICAgIDxuYW1lPlBpZXJyZS1MdWMgVmVpbGxldXg8L25hbWU+DQogICAgICAgICAgICA8ZW1haWw+cGx2ZWlsbGV1eEBhbG9naWVudC5jb208L2VtYWlsPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvbj5BbG9naWVudDwvb3JnYW5pemF0aW9uPg0KICAgICAgICAgICAgPG9yZ2FuaXphdGlvblVybD5odHRwOi8vd3d3LmFsb2dpZW50LmNvbTwvb3JnYW5pemF0aW9uVXJsPg0KICAgICAgICA8L2RldmVsb3Blcj4NCiAgICA8L2RldmVsb3BlcnM+DQoNCg0KICAgIDxsaWNlbnNlcz4NCiAgICAgICAgPGxpY2Vuc2U+DQogICAgICAgICAgICA8bmFtZT5UaGUgQXBhY2hlIFNvZnR3YXJlIExpY2Vuc2UsIFZlcnNpb24gMi4wPC9uYW1lPg0KICAgICAgICAgICAgPHVybD5odHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAudHh0PC91cmw+DQogICAgICAgICAgICA8ZGlzdHJpYnV0aW9uPnJlcG88L2Rpc3RyaWJ1dGlvbj4NCiAgICAgICAgPC9saWNlbnNlPg0KICAgIDwvbGljZW5zZXM+DQoNCiAgICA8YnVpbGQ+DQoNCiAgICAgICAgPHJlc291cmNlcz4NCiAgICAgICAgICAgIDxyZXNvdXJjZT4NCiAgICAgICAgICAgICAgICA8ZmlsdGVyaW5nPmZhbHNlPC9maWx0ZXJpbmc+DQogICAgICAgICAgICAgICAgPGRpcmVjdG9yeT5zcmMvbWFpbi9yZXNvdXJjZXM8L2RpcmVjdG9yeT4NCiAgICAgICAgICAgIDwvcmVzb3VyY2U+DQogICAgICAgICAgICA8cmVzb3VyY2U+DQogICAgICAgICAgICAgICAgPGZpbHRlcmluZz5mYWxzZTwvZmlsdGVyaW5nPg0KICAgICAgICAgICAgICAgIDxkaXJlY3Rvcnk+c3JjL21haW4vamF2YTwvZGlyZWN0b3J5Pg0KICAgICAgICAgICAgICAgIDxpbmNsdWRlcz4NCiAgICAgICAgICAgICAgICAgICAgPGluY2x1ZGU+Kio8L2luY2x1ZGU+DQogICAgICAgICAgICAgICAgPC9pbmNsdWRlcz4NCiAgICAgICAgICAgICAgICA8ZXhjbHVkZXM+DQogICAgICAgICAgICAgICAgICAgIDxleGNsdWRlPioqLyouamF2YTwvZXhjbHVkZT4NCiAgICAgICAgICAgICAgICA8L2V4Y2x1ZGVzPg0KICAgICAgICAgICAgPC9yZXNvdXJjZT4NCiAgICAgICAgPC9yZXNvdXJjZXM+DQogICAgICAgIDx0ZXN0UmVzb3VyY2VzPg0KICAgICAgICAgICAgPHRlc3RSZXNvdXJjZT4NCiAgICAgICAgICAgICAgICA8ZmlsdGVyaW5nPmZhbHNlPC9maWx0ZXJpbmc+DQogICAgICAgICAgICAgICAgPGRpcmVjdG9yeT5zcmMvdGVzdC9yZXNvdXJjZXM8L2RpcmVjdG9yeT4NCiAgICAgICAgICAgIDwvdGVzdFJlc291cmNlPg0KICAgICAgICAgICAgPHRlc3RSZXNvdXJjZT4NCiAgICAgICAgICAgICAgICA8ZmlsdGVyaW5nPmZhbHNlPC9maWx0ZXJpbmc+DQogICAgICAgICAgICAgICAgPGRpcmVjdG9yeT5zcmMvdGVzdC9qYXZhPC9kaXJlY3Rvcnk+DQogICAgICAgICAgICAgICAgPGluY2x1ZGVzPg0KICAgICAgICAgICAgICAgICAgICA8aW5jbHVkZT4qKjwvaW5jbHVkZT4NCiAgICAgICAgICAgICAgICA8L2luY2x1ZGVzPg0KICAgICAgICAgICAgICAgIDxleGNsdWRlcz4NCiAgICAgICAgICAgICAgICAgICAgPGV4Y2x1ZGU+KiovKi5qYXZhPC9leGNsdWRlPg0KICAgICAgICAgICAgICAgIDwvZXhjbHVkZXM+DQogICAgICAgICAgICA8L3Rlc3RSZXNvdXJjZT4NCiAgICAgICAgPC90ZXN0UmVzb3VyY2VzPg0KDQogICAgICAgIDxwbHVnaW5zPg0KICAgICAgICAgICAgPHBsdWdpbj4NCiAgICAgICAgICAgICAgICA8Z3JvdXBJZD5vcmcuYXBhY2hlLm1hdmVuLnBsdWdpbnM8L2dyb3VwSWQ+DQogICAgICAgICAgICAgICAgPGFydGlmYWN0SWQ+bWF2ZW4tY29tcGlsZXItcGx1Z2luPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgICAgIDx2ZXJzaW9uPjIuMy4yPC92ZXJzaW9uPg0KICAgICAgICAgICAgICAgIDxjb25maWd1cmF0aW9uPg0KICAgICAgICAgICAgICAgICAgICA8c291cmNlPjEuNjwvc291cmNlPg0KICAgICAgICAgICAgICAgICAgICA8dGFyZ2V0PjEuNjwvdGFyZ2V0Pg0KICAgICAgICAgICAgICAgIDwvY29uZmlndXJhdGlvbj4NCiAgICAgICAgICAgIDwvcGx1Z2luPg0KICAgICAgICAgICAgPHBsdWdpbj4NCiAgICAgICAgICAgICAgICA8Z3JvdXBJZD5vcmcuYXBhY2hlLm1hdmVuLnBsdWdpbnM8L2dyb3VwSWQ+DQogICAgICAgICAgICAgICAgPGFydGlmYWN0SWQ+bWF2ZW4tc291cmNlLXBsdWdpbjwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgICAgICA8dmVyc2lvbj4yLjEuMjwvdmVyc2lvbj4NCiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9ucz4NCiAgICAgICAgICAgICAgICAgICAgPGV4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxpZD5hdHRhY2gtc291cmNlczwvaWQ+DQogICAgICAgICAgICAgICAgICAgICAgICA8cGhhc2U+dmVyaWZ5PC9waGFzZT4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxnb2Fscz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Z29hbD5qYXI8L2dvYWw+DQogICAgICAgICAgICAgICAgICAgICAgICA8L2dvYWxzPg0KICAgICAgICAgICAgICAgICAgICA8L2V4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICA8L2V4ZWN1dGlvbnM+DQogICAgICAgICAgICA8L3BsdWdpbj4NCiAgICAgICAgICAgIDxwbHVnaW4+DQogICAgICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLmFwYWNoZS5tYXZlbi5wbHVnaW5zPC9ncm91cElkPg0KICAgICAgICAgICAgICAgIDxhcnRpZmFjdElkPm1hdmVuLWphdmFkb2MtcGx1Z2luPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgICAgIDx2ZXJzaW9uPjIuNzwvdmVyc2lvbj4NCiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9ucz4NCiAgICAgICAgICAgICAgICAgICAgPGV4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxpZD5hdHRhY2gtamF2YWRvY3M8L2lkPg0KICAgICAgICAgICAgICAgICAgICAgICAgPGdvYWxzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxnb2FsPmphcjwvZ29hbD4NCiAgICAgICAgICAgICAgICAgICAgICAgIDwvZ29hbHM+DQogICAgICAgICAgICAgICAgICAgIDwvZXhlY3V0aW9uPg0KICAgICAgICAgICAgICAgIDwvZXhlY3V0aW9ucz4NCiAgICAgICAgICAgIDwvcGx1Z2luPg0KICAgICAgICAgICAgPHBsdWdpbj4NCiAgICAgICAgICAgICAgICA8Z3JvdXBJZD5vcmcuYXBhY2hlLm1hdmVuLnBsdWdpbnM8L2dyb3VwSWQ+DQogICAgICAgICAgICAgICAgPGFydGlmYWN0SWQ+bWF2ZW4tcmVsZWFzZS1wbHVnaW48L2FydGlmYWN0SWQ+DQogICAgICAgICAgICAgICAgPHZlcnNpb24+Mi4xPC92ZXJzaW9uPg0KICAgICAgICAgICAgPC9wbHVnaW4+DQoJCQkJICAgIDxwbHVnaW4+DQogICAgICAJCQkJICA8Z3JvdXBJZD5vcmcuYXBhY2hlLm1hdmVuLnBsdWdpbnM8L2dyb3VwSWQ+DQogICAgICAgIAkJCQk8YXJ0aWZhY3RJZD5tYXZlbi1yZXBvc2l0b3J5LXBsdWdpbjwvYXJ0aWZhY3RJZD4NCiAgICAgICAgCQkJCTx2ZXJzaW9uPjIuMy4xPC92ZXJzaW9uPg0KICAgICAgCQkJPC9wbHVnaW4+DQogICAgICAgICAgICA8cGx1Z2luPg0KICAgICAgICAgICAgICAgIDxncm91cElkPm9yZy5hcGFjaGUubWF2ZW4ucGx1Z2luczwvZ3JvdXBJZD4NCiAgICAgICAgICAgICAgICA8YXJ0aWZhY3RJZD5tYXZlbi1ncGctcGx1Z2luPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgICAgIDx2ZXJzaW9uPjEuMTwvdmVyc2lvbj4NCiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9ucz4NCiAgICAgICAgICAgICAgICAgICAgPGV4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxpZD5zaWduLWFydGlmYWN0czwvaWQ+DQogICAgICAgICAgICAgICAgICAgICAgICA8cGhhc2U+dmVyaWZ5PC9waGFzZT4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxnb2Fscz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Z29hbD5zaWduPC9nb2FsPg0KICAgICAgICAgICAgICAgICAgICAgICAgPC9nb2Fscz4NCiAgICAgICAgICAgICAgICAgICAgPC9leGVjdXRpb24+DQogICAgICAgICAgICAgICAgPC9leGVjdXRpb25zPg0KICAgICAgICAgICAgPC9wbHVnaW4+DQogICAgICAJIDwvcGx1Z2lucz4NCiAgICA8L2J1aWxkPg0KDQogICAgPGRlcGVuZGVuY2llcz4NCg0KICAgICAgICA8IS0tIFNQUklORyBERVBFTkRFTkNJRVMgLS0+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1jb3JlPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+My4wLjQuUkVMRUFTRTwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxleGNsdXNpb25zPg0KICAgICAgICAgICAgICAgIDxleGNsdXNpb24+DQogICAgICAgICAgICAgICAgICAgIDxhcnRpZmFjdElkPmNvbW1vbnMtbG9nZ2luZzwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgICAgICAgICAgPGdyb3VwSWQ+Y29tbW9ucy1sb2dnaW5nPC9ncm91cElkPg0KICAgICAgICAgICAgICAgIDwvZXhjbHVzaW9uPg0KICAgICAgICAgICAgPC9leGNsdXNpb25zPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy13ZWI8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuNC5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1vcm08L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuNC5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1hc3BlY3RzPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+My4wLjQuUkVMRUFTRTwvdmVyc2lvbj4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy5zcHJpbmdmcmFtZXdvcms8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5zcHJpbmctZXhwcmVzc2lvbjwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjMuMC40LlJFTEVBU0U8L3ZlcnNpb24+DQogICAgICAgIDwvZGVwZW5kZW5jeT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5vcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5PC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+c3ByaW5nLXNlY3VyaXR5LWNvcmU8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuMy5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yay5zZWN1cml0eTwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1zZWN1cml0eS1jb25maWc8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuMy5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLnNwcmluZ2ZyYW1ld29yay5zZWN1cml0eTwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNwcmluZy1zZWN1cml0eS13ZWI8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4zLjAuMy5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQoNCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5jb20uc3ByaW5nc291cmNlLmluc2lnaHQ8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5pbnNpZ2h0LWFubm90YXRpb248L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjAuMC5SRUxFQVNFPC92ZXJzaW9uPg0KICAgICAgICAgICAgPHNjb3BlPnByb3ZpZGVkPC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KDQogICAgICAgIDwhLS0gU0VSVkxFVCBERVBFTkRFTkNJRVMgLS0+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+amF2YXguc2VydmxldDwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPnNlcnZsZXQtYXBpPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+Mi41PC92ZXJzaW9uPg0KICAgICAgICAgICAgPHNjb3BlPnByb3ZpZGVkPC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KDQogICAgICAgIDwhLS0gTE9HR0lORyBERVBFTkRFTkNJRVMgLSBMT0c0SiAtLT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5jaC5xb3MubG9nYmFjazwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPmxvZ2JhY2stY2xhc3NpYzwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjAuOS4yNjwvdmVyc2lvbj4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KDQogICAgICAgIDwhLS0gQVBBQ0hFIENPTU1PTlMgREVQRU5ERU5DSUVTIC0tPg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPmNvbW1vbnMtaW88L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5jb21tb25zLWlvPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+Mi4wPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+Y29tbW9ucy1sYW5nPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+Y29tbW9ucy1sYW5nPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+Mi41PC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLmFwYWNoZS5jb21tb25zPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+Y29tbW9ucy1lbWFpbDwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjEuMjwvdmVyc2lvbj4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPmNvbW1vbnMtYmVhbnV0aWxzPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+Y29tbW9ucy1iZWFudXRpbHM8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjguMzwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxleGNsdXNpb25zPg0KICAgICAgICAgICAgICAgIDxleGNsdXNpb24+DQogICAgICAgICAgICAgICAgICAgIDxhcnRpZmFjdElkPmNvbW1vbnMtbG9nZ2luZzwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgICAgICAgICAgPGdyb3VwSWQ+Y29tbW9ucy1sb2dnaW5nPC9ncm91cElkPg0KICAgICAgICAgICAgICAgIDwvZXhjbHVzaW9uPg0KICAgICAgICAgICAgPC9leGNsdXNpb25zPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+Y29tbW9ucy1jb2RlYzwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPmNvbW1vbnMtY29kZWM8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjQ8L3ZlcnNpb24+DQogICAgICAgIDwvZGVwZW5kZW5jeT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5jb21tb25zLWNvbmZpZ3VyYXRpb248L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5jb21tb25zLWNvbmZpZ3VyYXRpb248L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjY8L3ZlcnNpb24+DQogICAgICAgIDwvZGVwZW5kZW5jeT4NCg0KICAgICAgICA8IS0tIEhJQkVSTkFURSBERVBFTkRFTkNJRVMgLS0+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+b3JnLmhpYmVybmF0ZTwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPmhpYmVybmF0ZS1jb3JlPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+My42LjAuRmluYWw8L3ZlcnNpb24+DQogICAgICAgICAgICA8ZXhjbHVzaW9ucz4NCiAgICAgICAgICAgICAgICA8ZXhjbHVzaW9uPg0KICAgICAgICAgICAgICAgICAgICA8YXJ0aWZhY3RJZD5zbGY0ai1hcGk8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICAgICAgICAgIDxncm91cElkPm9yZy5zbGY0ajwvZ3JvdXBJZD4NCiAgICAgICAgICAgICAgICA8L2V4Y2x1c2lvbj4NCiAgICAgICAgICAgICAgICA8ZXhjbHVzaW9uPg0KICAgICAgICAgICAgICAgICAgICA8YXJ0aWZhY3RJZD5jb21tb25zLWNvbGxlY3Rpb25zPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgICAgICAgICA8Z3JvdXBJZD5jb21tb25zLWNvbGxlY3Rpb25zPC9ncm91cElkPg0KICAgICAgICAgICAgICAgIDwvZXhjbHVzaW9uPg0KICAgICAgICAgICAgPC9leGNsdXNpb25zPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+amF2YXNzaXN0PC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+amF2YXNzaXN0PC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+My4xMS4wLkdBPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+aHNxbGRiPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+aHNxbGRiPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+MS44LjAuMTA8L3ZlcnNpb24+DQogICAgICAgICAgICA8c2NvcGU+dGVzdDwvc2NvcGU+DQogICAgICAgIDwvZGVwZW5kZW5jeT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5jb20ubWljcm9zb2Z0PC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+c3FsamRiYzwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjIuMDwvdmVyc2lvbj4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KDQogICAgICAgIDwhLS0gVU5JVCBURVNUSU5HIC0tPg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy51bml0aWxzPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+dW5pdGlsczwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjIuNDwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxzY29wZT50ZXN0PC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPmp1bml0PC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+anVuaXQ8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj40LjguMjwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxzY29wZT50ZXN0PC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy5kYnVuaXQ8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5kYnVuaXQ8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4yLjQuODwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxzY29wZT50ZXN0PC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy5tb2NraXRvPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+bW9ja2l0by1hbGw8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICA8dmVyc2lvbj4xLjguNTwvdmVyc2lvbj4NCiAgICAgICAgICAgIDxzY29wZT50ZXN0PC9zY29wZT4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPnhtbHVuaXQ8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD54bWx1bml0PC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+MS4zPC92ZXJzaW9uPg0KICAgICAgICAgICAgPHNjb3BlPnRlc3Q8L3Njb3BlPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQoNCiAgICAgICAgPCEtLSBPVEhFUiAtLT4NCiAgICAgICAgPGRlcGVuZGVuY3k+DQogICAgICAgICAgICA8Z3JvdXBJZD5vcmcuYXBhY2hlLnNvbHI8L2dyb3VwSWQ+DQogICAgICAgICAgICA8YXJ0aWZhY3RJZD5zb2xyLXNvbHJqPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+MS40LjE8L3ZlcnNpb24+DQogICAgICAgICAgICA8ZXhjbHVzaW9ucz4NCiAgICAgICAgICAgICAgICA8ZXhjbHVzaW9uPg0KICAgICAgICAgICAgICAgICAgICA8YXJ0aWZhY3RJZD5zbGY0ai1hcGk8L2FydGlmYWN0SWQ+DQogICAgICAgICAgICAgICAgICAgIDxncm91cElkPm9yZy5zbGY0ajwvZ3JvdXBJZD4NCiAgICAgICAgICAgICAgICA8L2V4Y2x1c2lvbj4NCiAgICAgICAgICAgIDwvZXhjbHVzaW9ucz4NCiAgICAgICAgPC9kZXBlbmRlbmN5Pg0KICAgICAgICA8ZGVwZW5kZW5jeT4NCiAgICAgICAgICAgIDxncm91cElkPm9yZy5jb3MuY29tbW9uPC9ncm91cElkPg0KICAgICAgICAgICAgPGFydGlmYWN0SWQ+Y29zLXZhbGlkYXRvcjwvYXJ0aWZhY3RJZD4NCiAgICAgICAgICAgIDx2ZXJzaW9uPjEuMC4wPC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQogICAgICAgIDxkZXBlbmRlbmN5Pg0KICAgICAgICAgICAgPGdyb3VwSWQ+Y29tLmdvb2dsZS5ndWF2YTwvZ3JvdXBJZD4NCiAgICAgICAgICAgIDxhcnRpZmFjdElkPmd1YXZhPC9hcnRpZmFjdElkPg0KICAgICAgICAgICAgPHZlcnNpb24+cjA3PC92ZXJzaW9uPg0KICAgICAgICA8L2RlcGVuZGVuY3k+DQoNCiAgICA8L2RlcGVuZGVuY2llcz4NCg0KICAgIDxwcm9maWxlcz4NCiAgICAgICAgPHByb2ZpbGU+DQogICAgICAgICAgICA8aWQ+anJlYmVsPC9pZD4NCiAgICAgICAgICAgIDxhY3RpdmF0aW9uPg0KICAgICAgICAgICAgICAgIDxhY3RpdmVCeURlZmF1bHQ+ZmFsc2U8L2FjdGl2ZUJ5RGVmYXVsdD4NCiAgICAgICAgICAgIDwvYWN0aXZhdGlvbj4NCiAgICAgICAgICAgIDxidWlsZD4NCiAgICAgICAgICAgICAgICA8cGx1Z2lucz4NCiAgICAgICAgICAgICAgICAgICAgPHBsdWdpbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxncm91cElkPm9yZy56ZXJvdHVybmFyb3VuZDwvZ3JvdXBJZD4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxhcnRpZmFjdElkPmphdmFyZWJlbC1tYXZlbi1wbHVnaW48L2FydGlmYWN0SWQ+DQogICAgICAgICAgICAgICAgICAgICAgICA8dmVyc2lvbj4xLjAuNTwvdmVyc2lvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxleGVjdXRpb25zPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxleGVjdXRpb24+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpZD5nZW5lcmF0ZS1yZWJlbC14bWw8L2lkPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGhhc2U+cHJvY2Vzcy1yZXNvdXJjZXM8L3BoYXNlPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Z29hbHM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Z29hbD5nZW5lcmF0ZTwvZ29hbD4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9nb2Fscz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2V4ZWN1dGlvbj4NCiAgICAgICAgICAgICAgICAgICAgICAgIDwvZXhlY3V0aW9ucz4NCiAgICAgICAgICAgICAgICAgICAgPC9wbHVnaW4+DQogICAgICAgICAgICAgICAgPC9wbHVnaW5zPg0KICAgICAgICAgICAgPC9idWlsZD4NCiAgICAgICAgICAgIDxwcm9wZXJ0aWVzPg0KICAgICAgICAgICAgICAgIDxqZXR0eS1zY2FuSW50ZXJ2YWxTZWNvbmRzPjA8L2pldHR5LXNjYW5JbnRlcnZhbFNlY29uZHM+DQogICAgICAgICAgICA8L3Byb3BlcnRpZXM+DQogICAgICAgIDwvcHJvZmlsZT4NCiAgICA8L3Byb2ZpbGVzPg0KPC9wcm9qZWN0Pg0K diff --git a/syft/pkg/cataloger/package_exclusions.go b/syft/pkg/cataloger/package_exclusions.go index f99e088fc..c9d67dd61 100644 --- a/syft/pkg/cataloger/package_exclusions.go +++ b/syft/pkg/cataloger/package_exclusions.go @@ -1,7 +1,7 @@ package cataloger import ( - "golang.org/x/exp/slices" + "slices" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" diff --git a/syft/pkg/cataloger/rpm/parse_rpm_db.go b/syft/pkg/cataloger/rpm/parse_rpm_db.go index 02106f62c..1dee8c0f8 100644 --- a/syft/pkg/cataloger/rpm/parse_rpm_db.go +++ b/syft/pkg/cataloger/rpm/parse_rpm_db.go @@ -7,7 +7,6 @@ import ( rpmdb "github.com/knqyf263/go-rpmdb/pkg" - "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" @@ -18,7 +17,7 @@ import ( // parseRpmDb parses an "Packages" RPM DB and returns the Packages listed within it. func parseRpmDB(resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - f, err := os.CreateTemp("", internal.ApplicationName+"-rpmdb") + f, err := os.CreateTemp("", "rpmdb") if err != nil { return nil, nil, fmt.Errorf("failed to create temp rpmdb file: %w", err) } diff --git a/syft/pkg/conan_lock_metadata.go b/syft/pkg/conan_lock_metadata.go index 401c67481..3c20b7ca6 100644 --- a/syft/pkg/conan_lock_metadata.go +++ b/syft/pkg/conan_lock_metadata.go @@ -11,9 +11,9 @@ type ConanLockMetadata struct { Ref string `json:"ref"` PackageID string `json:"package_id,omitempty"` Prev string `json:"prev,omitempty"` - Requires string `json:"requires,omitempty"` - BuildRequires string `json:"build_requires,omitempty"` - PythonRequires string `json:"py_requires,omitempty"` + Requires []string `json:"requires,omitempty"` + BuildRequires []string `json:"build_requires,omitempty"` + PythonRequires []string `json:"py_requires,omitempty"` Options map[string]string `json:"options,omitempty"` Path string `json:"path,omitempty"` Context string `json:"context,omitempty"` diff --git a/syft/pkg/dpkg_metadata.go b/syft/pkg/dpkg_metadata.go index 2ad5de2db..5b38be03b 100644 --- a/syft/pkg/dpkg_metadata.go +++ b/syft/pkg/dpkg_metadata.go @@ -14,16 +14,49 @@ var _ FileOwner = (*DpkgMetadata)(nil) // DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described // at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section. +// Additional information about how these fields are used can be found at +// - https://www.debian.org/doc/debian-policy/ch-controlfields.html +// - https://www.debian.org/doc/debian-policy/ch-relationships.html +// - https://www.debian.org/doc/debian-policy/ch-binary.html#s-virtual-pkg +// - https://www.debian.org/doc/debian-policy/ch-relationships.html#s-virtual + type DpkgMetadata struct { - Package string `mapstructure:"Package" json:"package"` - Source string `mapstructure:"Source" json:"source" cyclonedx:"source"` - Version string `mapstructure:"Version" json:"version"` - SourceVersion string `mapstructure:"SourceVersion" json:"sourceVersion" cyclonedx:"sourceVersion"` - Architecture string `mapstructure:"Architecture" json:"architecture"` - Maintainer string `mapstructure:"Maintainer" json:"maintainer"` - InstalledSize int `mapstructure:"InstalledSize" json:"installedSize" cyclonedx:"installedSize"` - Description string `mapstructure:"Description" hash:"ignore" json:"-"` - Files []DpkgFileRecord `json:"files"` + Package string `json:"package"` + Source string `json:"source" cyclonedx:"source"` + Version string `json:"version"` + SourceVersion string `json:"sourceVersion" cyclonedx:"sourceVersion"` + + // Architecture can include the following sets of values depending on context and the control file used: + // - a unique single word identifying a Debian machine architecture as described in Architecture specification string (https://www.debian.org/doc/debian-policy/ch-customized-programs.html#s-arch-spec) . + // - an architecture wildcard identifying a set of Debian machine architectures, see Architecture wildcards (https://www.debian.org/doc/debian-policy/ch-customized-programs.html#s-arch-wildcard-spec). any matches all Debian machine architectures and is the most frequently used. + // - "all", which indicates an architecture-independent package. + // - "source", which indicates a source package. + Architecture string `json:"architecture"` + + // Maintainer is the package maintainer’s name and email address. The name must come first, then the email + // address inside angle brackets <> (in RFC822 format). + Maintainer string `json:"maintainer"` + + InstalledSize int `json:"installedSize" cyclonedx:"installedSize"` + + // Description contains a description of the binary package, consisting of two parts, the synopsis or the short + // description, and the long description (in a multiline format). + Description string `hash:"ignore" json:"-"` + + // Provides is a virtual package that is provided by one or more packages. A virtual package is one which appears + // in the Provides control field of another package. The effect is as if the package(s) which provide a particular + // virtual package name had been listed by name everywhere the virtual package name appears. (See also Virtual packages) + Provides []string `json:"provides,omitempty"` + + // Depends This declares an absolute dependency. A package will not be configured unless all of the packages listed in + // its Depends field have been correctly configured (unless there is a circular dependency). + Depends []string `json:"depends,omitempty"` + + // PreDepends is like Depends, except that it also forces dpkg to complete installation of the packages named + // before even starting the installation of the package which declares the pre-dependency. + PreDepends []string `json:"preDepends,omitempty"` + + Files []DpkgFileRecord `json:"files"` } // DpkgFileRecord represents a single file attributed to a debian package. diff --git a/syft/pkg/golang_metadata.go b/syft/pkg/golang_metadata.go index fd7309588..791b7fcc3 100644 --- a/syft/pkg/golang_metadata.go +++ b/syft/pkg/golang_metadata.go @@ -7,6 +7,7 @@ type GolangBinMetadata struct { Architecture string `json:"architecture" cyclonedx:"architecture"` H1Digest string `json:"h1Digest,omitempty" cyclonedx:"h1Digest"` MainModule string `json:"mainModule,omitempty" cyclonedx:"mainModule"` + GoCryptoSettings []string `json:"goCryptoSettings,omitempty" cyclonedx:"goCryptoSettings"` } // GolangModMetadata represents all captured data for a Golang source scan with go.mod/go.sum diff --git a/syft/pkg/license.go b/syft/pkg/license.go index 6e681da63..a6270ec9e 100644 --- a/syft/pkg/license.go +++ b/syft/pkg/license.go @@ -60,24 +60,17 @@ func (l Licenses) Swap(i, j int) { } func NewLicense(value string) License { - spdxExpression, err := license.ParseExpression(value) - if err != nil { - log.Trace("unable to parse license expression for %q: %w", value, err) - } - - return License{ - Value: value, - SPDXExpression: spdxExpression, - Type: license.Declared, - URLs: internal.NewStringSet(), - Locations: file.NewLocationSet(), - } + return NewLicenseFromType(value, license.Declared) } func NewLicenseFromType(value string, t license.Type) License { - spdxExpression, err := license.ParseExpression(value) - if err != nil { - log.Trace("unable to parse license expression: %w", err) + var spdxExpression string + if value != "" { + var err error + spdxExpression, err = license.ParseExpression(value) + if err != nil { + log.Trace("unable to parse license expression: %w", err) + } } return License{ @@ -124,6 +117,17 @@ func NewLicenseFromURLs(value string, urls ...string) License { return l } +func NewLicenseFromFields(value, url string, location *file.Location) License { + l := NewLicense(value) + if location != nil { + l.Locations.Add(*location) + } + if url != "" { + l.URLs.Add(url) + } + return l +} + // this is a bit of a hack to not infinitely recurse when hashing a license func (s License) Merge(l License) (*License, error) { sHash, err := artifact.IDByHash(s) diff --git a/syft/pkg/relationships.go b/syft/pkg/relationships.go index 8e3628cab..204dab2d9 100644 --- a/syft/pkg/relationships.go +++ b/syft/pkg/relationships.go @@ -1,9 +1,47 @@ package pkg -import "github.com/anchore/syft/syft/artifact" +import ( + "sort" + + "github.com/anchore/syft/syft/artifact" +) func NewRelationships(catalog *Collection) []artifact.Relationship { rels := RelationshipsByFileOwnership(catalog) rels = append(rels, RelationshipsEvidentBy(catalog)...) return rels } + +// SortRelationships takes a set of package-to-package relationships and sorts them in a stable order by name and version. +// Note: this does not consider package-to-other, other-to-package, or other-to-other relationships. +// TODO: ideally this should be replaced with a more type-agnostic sort function that resides in the artifact package. +func SortRelationships(rels []artifact.Relationship) { + sort.SliceStable(rels, func(i, j int) bool { + return relationshipLess(rels[i], rels[j]) + }) +} + +func relationshipLess(i, j artifact.Relationship) bool { + iFrom, ok1 := i.From.(Package) + iTo, ok2 := i.To.(Package) + jFrom, ok3 := j.From.(Package) + jTo, ok4 := j.To.(Package) + + if !(ok1 && ok2 && ok3 && ok4) { + return false + } + + if iFrom.Name != jFrom.Name { + return iFrom.Name < jFrom.Name + } + if iFrom.Version != jFrom.Version { + return iFrom.Version < jFrom.Version + } + if iTo.Name != jTo.Name { + return iTo.Name < jTo.Name + } + if iTo.Version != jTo.Version { + return iTo.Version < jTo.Version + } + return i.Type < j.Type +} diff --git a/syft/pkg/type.go b/syft/pkg/type.go index e3ed3f4c1..788e584a5 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -9,34 +9,36 @@ type Type string const ( // the full set of supported packages - UnknownPkg Type = "UnknownPackage" - AlpmPkg Type = "alpm" - ApkPkg Type = "apk" - BinaryPkg Type = "binary" - CocoapodsPkg Type = "pod" - ConanPkg Type = "conan" - DartPubPkg Type = "dart-pub" - DebPkg Type = "deb" - DotnetPkg Type = "dotnet" - GemPkg Type = "gem" - GoModulePkg Type = "go-module" - GraalVMNativeImagePkg Type = "graalvm-native-image" - HackagePkg Type = "hackage" - HexPkg Type = "hex" - JavaPkg Type = "java-archive" - JenkinsPluginPkg Type = "jenkins-plugin" - KbPkg Type = "msrc-kb" - LinuxKernelPkg Type = "linux-kernel" - LinuxKernelModulePkg Type = "linux-kernel-module" - NixPkg Type = "nix" - NpmPkg Type = "npm" - PhpComposerPkg Type = "php-composer" - PortagePkg Type = "portage" - PythonPkg Type = "python" - Rpkg Type = "R-package" - RpmPkg Type = "rpm" - RustPkg Type = "rust-crate" - SwiftPkg Type = "swift" + UnknownPkg Type = "UnknownPackage" + AlpmPkg Type = "alpm" + ApkPkg Type = "apk" + BinaryPkg Type = "binary" + CocoapodsPkg Type = "pod" + ConanPkg Type = "conan" + DartPubPkg Type = "dart-pub" + DebPkg Type = "deb" + DotnetPkg Type = "dotnet" + GemPkg Type = "gem" + GithubActionPkg Type = "github-action" + GithubActionWorkflowPkg Type = "github-action-workflow" + GoModulePkg Type = "go-module" + GraalVMNativeImagePkg Type = "graalvm-native-image" + HackagePkg Type = "hackage" + HexPkg Type = "hex" + JavaPkg Type = "java-archive" + JenkinsPluginPkg Type = "jenkins-plugin" + KbPkg Type = "msrc-kb" + LinuxKernelPkg Type = "linux-kernel" + LinuxKernelModulePkg Type = "linux-kernel-module" + NixPkg Type = "nix" + NpmPkg Type = "npm" + PhpComposerPkg Type = "php-composer" + PortagePkg Type = "portage" + PythonPkg Type = "python" + Rpkg Type = "R-package" + RpmPkg Type = "rpm" + RustPkg Type = "rust-crate" + SwiftPkg Type = "swift" ) // AllPkgs represents all supported package types @@ -50,6 +52,8 @@ var AllPkgs = []Type{ DebPkg, DotnetPkg, GemPkg, + GithubActionPkg, + GithubActionWorkflowPkg, GoModulePkg, HackagePkg, HexPkg, @@ -70,6 +74,8 @@ var AllPkgs = []Type{ } // PackageURLType returns the PURL package type for the current package. +// +//nolint:funlen func (t Type) PackageURLType() string { switch t { case AlpmPkg: @@ -90,6 +96,9 @@ func (t Type) PackageURLType() string { return packageurl.TypeGem case HexPkg: return packageurl.TypeHex + case GithubActionPkg, GithubActionWorkflowPkg: + // note: this is not a real purl type, but it is the closest thing we have for now + return packageurl.TypeGithub case GoModulePkg: return packageurl.TypeGolang case HackagePkg: diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index 3da73184d..64d8e87c3 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -114,6 +114,7 @@ func TestTypeFromPURL(t *testing.T) { expectedTypes.Remove(string(PortagePkg)) expectedTypes.Remove(string(BinaryPkg)) expectedTypes.Remove(string(LinuxKernelModulePkg)) + expectedTypes.Remove(string(GithubActionPkg), string(GithubActionWorkflowPkg)) for _, test := range tests { t.Run(string(test.expected), func(t *testing.T) { diff --git a/syft/sbom/sbom.go b/syft/sbom/sbom.go index f95df0955..18058255b 100644 --- a/syft/sbom/sbom.go +++ b/syft/sbom/sbom.go @@ -1,10 +1,9 @@ package sbom import ( + "slices" "sort" - "golang.org/x/exp/slices" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" diff --git a/syft/source/detection.go b/syft/source/detection.go index f96dc0023..84c622a16 100644 --- a/syft/source/detection.go +++ b/syft/source/detection.go @@ -199,6 +199,8 @@ func parseDefaultImageSource(defaultImageSource string) image.Source { return image.DockerDaemonSource case "podman": return image.PodmanDaemonSource + case "containerd": + return image.ContainerdDaemonSource default: return image.UnknownSource } diff --git a/syft/source/detection_test.go b/syft/source/detection_test.go index 380ca8e65..dd8a0276e 100644 --- a/syft/source/detection_test.go +++ b/syft/source/detection_test.go @@ -247,6 +247,26 @@ func Test_Detect(t *testing.T) { expectedScheme: unknownType, expectedLocation: "", }, + { + name: "podman-image", + userInput: "containerd:anchore/syft", + detection: detectorResult{ + src: image.PodmanDaemonSource, + ref: "anchore/syft", + }, + expectedScheme: containerImageType, + expectedLocation: "anchore/syft", + }, + { + name: "containerd-image", + userInput: "containerd:anchore/syft", + detection: detectorResult{ + src: image.ContainerdDaemonSource, + ref: "anchore/syft", + }, + expectedScheme: containerImageType, + expectedLocation: "anchore/syft", + }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { diff --git a/syft/source/stereoscope_image_source.go b/syft/source/stereoscope_image_source.go index 2f45de7de..fc5defeed 100644 --- a/syft/source/stereoscope_image_source.go +++ b/syft/source/stereoscope_image_source.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/bmatcuk/doublestar/v4" - "github.com/docker/distribution/reference" + "github.com/distribution/reference" "github.com/opencontainers/go-digest" "github.com/anchore/stereoscope" diff --git a/test/cli/packages_cmd_test.go b/test/cli/packages_cmd_test.go index 41bc2bcc0..81ad258b1 100644 --- a/test/cli/packages_cmd_test.go +++ b/test/cli/packages_cmd_test.go @@ -6,6 +6,12 @@ import ( "testing" ) +const ( + // this is the number of packages that should be found in the image-pkg-coverage fixture image + // when analyzed with the squashed scope. + coverageImageSquashedPackageCount = 24 +) + func TestPackagesCmdFlags(t *testing.T) { hiddenPackagesImage := "docker-archive:" + getFixtureImage(t, "image-hidden-packages") coverageImage := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage") @@ -36,6 +42,24 @@ func TestPackagesCmdFlags(t *testing.T) { assertSuccessfulReturnCode, }, }, + { + name: "quiet-flag-with-logger", + args: []string{"packages", "-qvv", "-o", "json", coverageImage}, + assertions: []traitAssertion{ + assertJsonReport, + assertNoStderr, + assertSuccessfulReturnCode, + }, + }, + { + name: "quiet-flag-with-tui", + args: []string{"packages", "-q", "-o", "json", coverageImage}, + assertions: []traitAssertion{ + assertJsonReport, + assertNoStderr, + assertSuccessfulReturnCode, + }, + }, { name: "multiple-output-flags", args: []string{"packages", "-o", "table", "-o", "json=" + tmp + ".tmp/multiple-output-flag-test.json", coverageImage}, @@ -96,7 +120,7 @@ func TestPackagesCmdFlags(t *testing.T) { name: "squashed-scope-flag", args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage}, assertions: []traitAssertion{ - assertPackageCount(24), + assertPackageCount(coverageImageSquashedPackageCount), assertSuccessfulReturnCode, }, }, @@ -153,7 +177,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "responds-to-package-cataloger-search-options", - args: []string{"packages", "-vv"}, + args: []string{"--help"}, env: map[string]string{ "SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES": "true", "SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES": "false", @@ -213,7 +237,7 @@ func TestPackagesCmdFlags(t *testing.T) { // the application config in the log matches that of what we expect to have been configured. assertInOutput("parallelism: 2"), assertInOutput("parallelism=2"), - assertPackageCount(24), + assertPackageCount(coverageImageSquashedPackageCount), assertSuccessfulReturnCode, }, }, @@ -224,7 +248,7 @@ func TestPackagesCmdFlags(t *testing.T) { // the application config in the log matches that of what we expect to have been configured. assertInOutput("parallelism: 1"), assertInOutput("parallelism=1"), - assertPackageCount(24), + assertPackageCount(coverageImageSquashedPackageCount), assertSuccessfulReturnCode, }, }, @@ -238,7 +262,7 @@ func TestPackagesCmdFlags(t *testing.T) { assertions: []traitAssertion{ assertNotInOutput("secret_password"), assertNotInOutput("secret_key_path"), - assertPackageCount(24), + assertPackageCount(coverageImageSquashedPackageCount), assertSuccessfulReturnCode, }, }, @@ -258,7 +282,7 @@ func TestPackagesCmdFlags(t *testing.T) { func TestRegistryAuth(t *testing.T) { host := "localhost:17" image := fmt.Sprintf("%s/something:latest", host) - args := []string{"packages", "-vv", fmt.Sprintf("registry:%s", image)} + args := []string{"packages", "-vvv", fmt.Sprintf("registry:%s", image)} tests := []struct { name string @@ -272,7 +296,7 @@ func TestRegistryAuth(t *testing.T) { assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), assertInOutput(image), - assertInOutput("no registry credentials configured, using the default keychain"), + assertInOutput(fmt.Sprintf("no registry credentials configured for %q, using the default keychain", host)), }, }, { @@ -294,7 +318,7 @@ func TestRegistryAuth(t *testing.T) { args: args, env: map[string]string{ "SYFT_REGISTRY_AUTH_AUTHORITY": host, - "SYFT_REGISTRY_AUTH_TOKEN": "token", + "SYFT_REGISTRY_AUTH_TOKEN": "my-token", }, assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), @@ -303,7 +327,7 @@ func TestRegistryAuth(t *testing.T) { }, }, { - name: "not enough info fallsback to keychain", + name: "not enough info fallback to keychain", args: args, env: map[string]string{ "SYFT_REGISTRY_AUTH_AUTHORITY": host, @@ -311,7 +335,7 @@ func TestRegistryAuth(t *testing.T) { assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), assertInOutput(image), - assertInOutput(`no registry credentials configured, using the default keychain`), + assertInOutput(fmt.Sprintf(`no registry credentials configured for %q, using the default keychain`, host)), }, }, { @@ -324,6 +348,17 @@ func TestRegistryAuth(t *testing.T) { assertInOutput("insecure-use-http: true"), }, }, + { + name: "use tls configuration", + args: args, + env: map[string]string{ + "SYFT_REGISTRY_AUTH_TLS_CERT": "place.crt", + "SYFT_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-bad-binaries/Dockerfile b/test/cli/test-fixtures/image-bad-binaries/Dockerfile index 7851a945b..db48c26f8 100644 --- a/test/cli/test-fixtures/image-bad-binaries/Dockerfile +++ b/test/cli/test-fixtures/image-bad-binaries/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:sid +FROM debian:sid@sha256:bee393b48f83fdeb978ce68abb9dc955d6398b984fca88b52a09dceb45ac74b5 ADD sources.list /etc/apt/sources.list.d/sources.list RUN apt update -y && apt install -y dpkg-dev # this as a "macho-invalid" directory which is useful for testing diff --git a/test/cli/test-fixtures/image-hidden-packages/Dockerfile b/test/cli/test-fixtures/image-hidden-packages/Dockerfile index cf8ea3bfa..1150209e8 100644 --- a/test/cli/test-fixtures/image-hidden-packages/Dockerfile +++ b/test/cli/test-fixtures/image-hidden-packages/Dockerfile @@ -1,4 +1,4 @@ -FROM centos:7.9.2009 +FROM centos:7.9.2009@sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f # all-layers scope should pickup on vsftpd RUN yum install -y vsftpd -RUN yum remove -y vsftpd \ No newline at end of file +RUN yum remove -y vsftpd diff --git a/test/cli/test-fixtures/image-java-spdx-tools/Dockerfile b/test/cli/test-fixtures/image-java-spdx-tools/Dockerfile index 36a3da7b6..938ca24d0 100644 --- a/test/cli/test-fixtures/image-java-spdx-tools/Dockerfile +++ b/test/cli/test-fixtures/image-java-spdx-tools/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:11 +FROM openjdk:11@sha256:e81b7f317654b0f26d3993e014b04bcb29250339b11b9de41e130feecd4cd43c RUN wget --no-verbose https://github.com/spdx/tools-java/releases/download/v1.1.3/tools-java-1.1.3.zip && \ unzip tools-java-1.1.3.zip && \ diff --git a/test/cli/test-fixtures/image-node-binary/Dockerfile b/test/cli/test-fixtures/image-node-binary/Dockerfile index 6bda80cab..53a785448 100644 --- a/test/cli/test-fixtures/image-node-binary/Dockerfile +++ b/test/cli/test-fixtures/image-node-binary/Dockerfile @@ -1 +1 @@ -FROM node:19-alpine3.15 \ No newline at end of file +FROM node:19-alpine3.15@sha256:07050181369f52460e788738272bcb22b71c7cfc6fafee0b5cd27d2022513a86 diff --git a/test/cli/test-fixtures/registry/Dockerfile b/test/cli/test-fixtures/registry/Dockerfile index b09b037ca..294558cee 100644 --- a/test/cli/test-fixtures/registry/Dockerfile +++ b/test/cli/test-fixtures/registry/Dockerfile @@ -1 +1 @@ -FROM alpine:latest +FROM alpine@sha256:c5c5fda71656f28e49ac9c5416b3643eaa6a108a8093151d6d1afc9463be8e33 diff --git a/test/cli/trait_assertions_test.go b/test/cli/trait_assertions_test.go index 465c389d2..828b407bf 100644 --- a/test/cli/trait_assertions_test.go +++ b/test/cli/trait_assertions_test.go @@ -4,8 +4,6 @@ import ( "encoding/json" "fmt" "os" - "os/exec" - "path/filepath" "regexp" "sort" "strings" @@ -85,11 +83,20 @@ func assertNotInOutput(data string) traitAssertion { } } +func assertNoStderr(tb testing.TB, _, stderr string, _ int) { + tb.Helper() + if len(stderr) > 0 { + tb.Errorf("expected stderr to be empty, but got %q", stderr) + } +} + func assertInOutput(data string) traitAssertion { return func(tb testing.TB, stdout, stderr string, _ int) { tb.Helper() - if !strings.Contains(stripansi.Strip(stderr), data) && !strings.Contains(stripansi.Strip(stdout), data) { - tb.Errorf("data=%q was NOT found in any output, but should have been there", data) + stdout = stripansi.Strip(stdout) + stderr = stripansi.Strip(stderr) + if !strings.Contains(stdout, data) && !strings.Contains(stderr, data) { + tb.Errorf("data=%q was NOT found in any output, but should have been there\nSTDOUT:%s\nSTDERR:%s", data, stdout, stderr) } } } @@ -148,46 +155,6 @@ func assertSuccessfulReturnCode(tb testing.TB, _, _ string, rc int) { } } -func assertVerifyAttestation(coverageImage string) traitAssertion { - return func(tb testing.TB, stdout, _ string, _ int) { - tb.Helper() - cosignPath := filepath.Join(repoRoot(tb), ".tmp/cosign") - err := os.WriteFile("attestation.json", []byte(stdout), 0664) - if err != nil { - tb.Errorf("could not write attestation to disk") - } - defer os.Remove("attestation.json") - attachCmd := exec.Command( - cosignPath, - "attach", - "attestation", - "--attestation", - "attestation.json", - coverageImage, // TODO which remote image to use? - ) - - stdout, stderr, _ := runCommand(attachCmd, nil) - if attachCmd.ProcessState.ExitCode() != 0 { - tb.Log("STDOUT", stdout) - tb.Log("STDERR", stderr) - tb.Fatalf("could not attach image") - } - - verifyCmd := exec.Command( - cosignPath, - "verify-attestation", - coverageImage, // TODO which remote image to use? - ) - - stdout, stderr, _ = runCommand(verifyCmd, nil) - if attachCmd.ProcessState.ExitCode() != 0 { - tb.Log("STDOUT", stdout) - tb.Log("STDERR", stderr) - tb.Fatalf("could not verify attestation") - } - } -} - func assertFileExists(file string) traitAssertion { return func(tb testing.TB, _, _ string, _ int) { tb.Helper() diff --git a/test/install/environments/Dockerfile-alpine-3.6 b/test/install/environments/Dockerfile-alpine-3.6 index 982e54029..f0a5fcf72 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 dafb64ed7..07341fc38 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/catalog_packages_cases_test.go b/test/integration/catalog_packages_cases_test.go index 48b280a56..52bd6f287 100644 --- a/test/integration/catalog_packages_cases_test.go +++ b/test/integration/catalog_packages_cases_test.go @@ -247,6 +247,8 @@ var dirOnlyTestCases = []testCase{ "Serilog.Sinks.Console": "4.0.1", "System.Diagnostics.DiagnosticSource": "6.0.0", "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "TestCommon": "1.0.0", + "TestLibrary": "1.0.0", }, }, { @@ -368,6 +370,20 @@ var dirOnlyTestCases = []testCase{ "swift-numerics": "1.0.2", }, }, + { + name: "find github action packages (from usage in workflow files and composite actions)", + pkgType: pkg.GithubActionPkg, + pkgInfo: map[string]string{ + "actions/checkout": "v4", + }, + }, + { + name: "find github shared workflow calls (from usage in workflow files)", + pkgType: pkg.GithubActionWorkflowPkg, + pkgInfo: map[string]string{ + "octo-org/this-repo/.github/workflows/workflow-1.yml": "172239021f7ba04fe7327647b213799853a9eb89", + }, + }, } var commonTestCases = []testCase{ diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index 4596a75aa..50a8b06ba 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -23,7 +23,7 @@ func BenchmarkImagePackageCatalogers(b *testing.B) { tarPath := imagetest.GetFixtureImageTarPath(b, fixtureImageName) var pc *pkg.Collection - for _, c := range cataloger.ImageCatalogers(defaultConfig()) { + for _, c := range cataloger.ImageCatalogers(cataloger.DefaultConfig()) { // in case of future alteration where state is persisted, assume no dependency is safe to reuse userInput := "docker-archive:" + tarPath detection, err := source.Detect(userInput, source.DefaultDetectConfig()) @@ -96,6 +96,8 @@ func TestPkgCoverageImage(t *testing.T) { definedPkgs.Remove(string(pkg.LinuxKernelPkg)) definedPkgs.Remove(string(pkg.LinuxKernelModulePkg)) definedPkgs.Remove(string(pkg.SwiftPkg)) + definedPkgs.Remove(string(pkg.GithubActionPkg)) + definedPkgs.Remove(string(pkg.GithubActionWorkflowPkg)) var cases []testCase cases = append(cases, commonTestCases...) @@ -260,7 +262,7 @@ func TestPkgCoverageCatalogerConfiguration(t *testing.T) { assert.Equal(t, definedLanguages, observedLanguages) // Verify that rust isn't actually an image cataloger - c := defaultConfig() + c := cataloger.DefaultConfig() c.Catalogers = []string{"rust"} assert.Len(t, cataloger.ImageCatalogers(c), 0) } diff --git a/test/integration/convert_test.go b/test/integration/convert_test.go index d20cab27b..f1c0eb326 100644 --- a/test/integration/convert_test.go +++ b/test/integration/convert_test.go @@ -1,15 +1,14 @@ package integration import ( - "context" "fmt" "os" "testing" "github.com/stretchr/testify/require" - "github.com/anchore/syft/cmd/syft/cli/convert" - "github.com/anchore/syft/internal/config" + "github.com/anchore/syft/cmd/syft/cli/commands" + "github.com/anchore/syft/cmd/syft/cli/options" "github.com/anchore/syft/syft/formats" "github.com/anchore/syft/syft/formats/cyclonedxjson" "github.com/anchore/syft/syft/formats/cyclonedxxml" @@ -72,9 +71,10 @@ func TestConvertCmd(t *testing.T) { _ = os.Remove(syftFile.Name()) }() - ctx := context.Background() - app := &config.Application{ - Outputs: []string{fmt.Sprintf("%s=%s", test.format.ID().String(), formatFile.Name())}, + opts := &commands.ConvertOptions{ + MultiOutput: options.MultiOutput{ + Outputs: []string{fmt.Sprintf("%s=%s", test.format.ID().String(), formatFile.Name())}, + }, } // stdout reduction of test noise @@ -84,7 +84,7 @@ func TestConvertCmd(t *testing.T) { os.Stdout = rescue }() - err = convert.Run(ctx, app, []string{syftFile.Name()}) + err = commands.RunConvert(opts, syftFile.Name()) require.NoError(t, err) contents, err := os.ReadFile(formatFile.Name()) require.NoError(t, err) diff --git a/test/integration/encode_decode_cycle_test.go b/test/integration/encode_decode_cycle_test.go index 844959bfe..ce839eca6 100644 --- a/test/integration/encode_decode_cycle_test.go +++ b/test/integration/encode_decode_cycle_test.go @@ -47,8 +47,8 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { formatOption: cyclonedxjson.ID, redactor: func(in []byte) []byte { // unstable values - in = regexp.MustCompile(`"(timestamp|serialNumber|bom-ref)": "[^"]+",`).ReplaceAll(in, []byte{}) - + in = regexp.MustCompile(`"(timestamp|serialNumber|bom-ref|ref)":\s*"(\n|[^"])+"`).ReplaceAll(in, []byte(`"$1": "redacted"`)) + in = regexp.MustCompile(`"(dependsOn)":\s*\[(?:\s|[^]])+]`).ReplaceAll(in, []byte(`"$1": []`)) return in }, json: true, @@ -57,7 +57,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { formatOption: cyclonedxxml.ID, redactor: func(in []byte) []byte { // unstable values - in = regexp.MustCompile(`(serialNumber|bom-ref)="[^"]+"`).ReplaceAll(in, []byte{}) + in = regexp.MustCompile(`(serialNumber|bom-ref|ref)="[^"]+"`).ReplaceAll(in, []byte{}) in = regexp.MustCompile(`[^<]+`).ReplaceAll(in, []byte{}) return in diff --git a/test/integration/go_compiler_detection_test.go b/test/integration/go_compiler_detection_test.go new file mode 100644 index 000000000..269944823 --- /dev/null +++ b/test/integration/go_compiler_detection_test.go @@ -0,0 +1,62 @@ +package integration + +import ( + "testing" + + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/source" +) + +func TestGolangCompilerDetection(t *testing.T) { + tests := []struct { + name string + image string + expectedCompilers []string + expectedCPE []cpe.CPE + expectedPURL []string + }{ + { + name: "syft can detect a single golang compiler given the golang base image", + image: "image-golang-compiler", + expectedCompilers: []string{"go1.18.10"}, + expectedCPE: []cpe.CPE{cpe.Must("cpe:2.3:a:golang:go:1.18.10:-:*:*:*:*:*:*")}, + expectedPURL: []string{"pkg:golang/stdlib@1.18.10"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sbom, _ := catalogFixtureImage(t, tt.image, source.SquashedScope, nil) + packages := sbom.Artifacts.Packages.PackagesByName("stdlib") + + foundCompilerVersions := make(map[string]struct{}) + foundCPE := make(map[cpe.CPE]struct{}) + foundPURL := make(map[string]struct{}) + + for _, pkg := range packages { + foundCompilerVersions[pkg.Version] = struct{}{} + foundPURL[pkg.PURL] = struct{}{} + for _, cpe := range pkg.CPEs { + foundCPE[cpe] = struct{}{} + } + } + + for _, expectedCompiler := range tt.expectedCompilers { + if _, ok := foundCompilerVersions[expectedCompiler]; !ok { + t.Fatalf("expected %s version; not found in found compilers: %v", expectedCompiler, foundCompilerVersions) + } + } + + for _, expectedPURL := range tt.expectedPURL { + if _, ok := foundPURL[expectedPURL]; !ok { + t.Fatalf("expected %s purl; not found in found purl: %v", expectedPURL, expectedPURLs) + } + } + + for _, expectedCPE := range tt.expectedCPE { + if _, ok := foundCPE[expectedCPE]; !ok { + t.Fatalf("expected %s version; not found in found cpe: %v", expectedCPE, expectedCPE) + } + } + }) + } +} diff --git a/test/integration/java_purl_test.go b/test/integration/java_purl_test.go new file mode 100644 index 000000000..15ed6dd0d --- /dev/null +++ b/test/integration/java_purl_test.go @@ -0,0 +1,197 @@ +package integration + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func TestJavaPURLs(t *testing.T) { + sbom, _ := catalogFixtureImage(t, "image-test-java-purls", source.SquashedScope, nil) + found := make(map[string]string) + for _, p := range sbom.Artifacts.Packages.Sorted() { + if p.Type != pkg.JavaPkg && p.Type != pkg.JenkinsPluginPkg { + continue + } + key := fmt.Sprintf("%s@%s", p.Name, p.Version) + found[key] = p.PURL + } + for key, expectedPURL := range expectedPURLs { + purl := found[key] + assert.Equal(t, expectedPURL, purl, fmt.Sprintf("found wrong or missing PURL for %s want %s, got %s", key, expectedPURL, purl)) + } + for key, foundPURL := range found { + expectedPURL := expectedPURLs[key] + assert.Equal(t, expectedPURL, foundPURL, fmt.Sprintf("found extra purl for %s want %s, got %s", key, expectedPURL, foundPURL)) + } +} + +// Constructed by: +// syft anchore/test_images:java-56d52bc -o template -t /tmp/test.templ | grep 'pkg:maven' | sort | uniq >> test/integration/java_purl_test.go +// where the template is: +/* +{{ range .Artifacts}}"{{.Name}}@{{.Version}}":"{{.PURL}}", +{{ end }} +*/ +// The map was then hand-edited for correctness by comparing to Maven Central. +var expectedPURLs = map[string]string{ + "TwilioNotifier@0.2.1": "pkg:maven/com.twilio.jenkins/TwilioNotifier@0.2.1", + "access-modifier-annotation@1.0": "pkg:maven/org.kohsuke/access-modifier-annotation@1.0", + "acegi-security@1.0.5": "pkg:maven/org.acegisecurity/acegi-security@1.0.5", + "activation@1.1.1-hudson-1": "pkg:maven/org.jvnet.hudson/activation@1.1.1-hudson-1", + "akuma@1.2": "pkg:maven/com.sun.akuma/akuma@1.2", + "animal-sniffer-annotation@1.0": "pkg:maven/org.jvnet/animal-sniffer-annotation@1.0", + "annotation-indexer@1.2": "pkg:maven/org.jvnet.hudson/annotation-indexer@1.2", + "annotations@13.0": "pkg:maven/org.jetbrains/annotations@13.0", + "ant-launcher@1.8.0": "pkg:maven/org.apache.ant/ant-launcher@1.8.0", + "ant@1.8.0": "pkg:maven/org.apache.ant/ant@1.8.0", + "antlr@2.7.6": "pkg:maven/antlr/antlr@2.7.6", + "aopalliance@1.0": "pkg:maven/aopalliance/aopalliance@1.0", + "args4j@2.0.16": "pkg:maven/args4j/args4j@2.0.16", + "asm-commons@2.2.3": "pkg:maven/asm-commons/asm-commons@2.2.3", + "asm-tree@2.2.3": "pkg:maven/asm-tree/asm-tree@2.2.3", + "asm@2.2.3": "pkg:maven/asm/asm@2.2.3", + "avalon-framework@4.1.3": "pkg:maven/avalon-framework/avalon-framework@4.1.3", + "bridge-method-annotation@1.2": "pkg:maven/com.infradna.tool/bridge-method-annotation@1.2", + "classworlds@1.1": "pkg:maven/org.codehaus.classworlds/classworlds@1.1", + "cli@1.390": "pkg:maven/org.jvnet.hudson.main/cli@1.390", + "commons-beanutils@1.8.0": "pkg:maven/commons-beanutils/commons-beanutils@1.8.0", + "commons-codec@1.2": "pkg:maven/commons-codec/commons-codec@1.2", + "commons-codec@1.4": "pkg:maven/commons-codec/commons-codec@1.4", + "commons-collections@3.2": "pkg:maven/commons-collections/commons-collections@3.2", + "commons-digester@1.7": "pkg:maven/commons-digester/commons-digester@1.7", + "commons-discovery@0.4": "pkg:maven/commons-discovery/commons-discovery@0.4", + "commons-fileupload@1.2.1": "pkg:maven/commons-fileupload/commons-fileupload@1.2.1", + "commons-httpclient@3.1": "pkg:maven/org.apache/commons-httpclient@3.1", + "commons-httpclient@3.1-rc1": "pkg:maven/commons-httpclient/commons-httpclient@3.1-rc1", + "commons-io@1.4": "pkg:maven/commons-io/commons-io@1.4", + "commons-jelly-tags-define@1.0.1-hudson-20071021": "pkg:maven/org.jvnet.hudson/commons-jelly-tags-define@1.0.1-hudson-20071021", + "commons-jelly-tags-fmt@1.0": "pkg:maven/commons-jelly-tags-fmt/commons-jelly-tags-fmt@1.0", + "commons-jelly-tags-xml@1.1": "pkg:maven/commons-jelly-tags-xml/commons-jelly-tags-xml@1.1", + "commons-jelly@1.1-hudson-20100305": "pkg:maven/org.jvnet.hudson/commons-jelly@1.1-hudson-20100305", + "commons-jexl@1.1-hudson-20090508": "pkg:maven/org.jvnet.hudson/commons-jexl@1.1-hudson-20090508", + "commons-lang@2.4": "pkg:maven/commons-lang/commons-lang@2.4", + "commons-lang@2.5": "pkg:maven/commons-lang/commons-lang@2.5", + "commons-logging@1.0.4": "pkg:maven/commons-logging/commons-logging@1.0.4", // see https://mvnrepository.com/artifact/commons-logging/commons-logging/1.0.4 + "commons-logging@1.1": "pkg:maven/commons-logging/commons-logging@1.1", // see https://mvnrepository.com/artifact/commons-logging/commons-logging/1.1 + "commons-logging@1.1.1": "pkg:maven/commons-logging/commons-logging@1.1.1", // see https://mvnrepository.com/artifact/commons-logging/commons-logging/1.1.1 + "commons-pool@1.3": "pkg:maven/commons-pool/commons-pool@1.3", + "crypto-util@1.0": "pkg:maven/org.jvnet.hudson/crypto-util@1.0", + "cvs@1.2": "pkg:maven/org.jvnet.hudson.plugins/cvs@1.2", + "dom4j@1.6.1-hudson-3": "pkg:maven/dom4j/dom4j@1.6.1-hudson-3", + "doxia-sink-api@1.0-alpha-10": "pkg:maven/org.apache.maven.doxia/doxia-sink-api@1.0-alpha-10", + "easymock@2.4": "pkg:maven/org.easymock/easymock@2.4", + "embedded_su4j@1.1": "pkg:maven/com.sun.solaris/embedded_su4j@1.1", + "example-java-app-gradle@0.1.0": "pkg:maven/example-java-app-gradle/example-java-app-gradle@0.1.0", + "ezmorph@1.0.3": "pkg:maven/net.sf.ezmorph/ezmorph@1.0.3", + "graph-layouter@1.0": "pkg:maven/org.kohsuke/graph-layouter@1.0", + "groovy-all@1.6.0": "pkg:maven/groovy-all/groovy-all@1.6.0", + "gson@2.8.6": "pkg:maven/com.google.code.gson/gson@2.8.6", + "guava@r06": "pkg:maven/com.google.guava/guava@r06", + "httpclient@4.1.1": "pkg:maven/org.apache.httpcomponents/httpclient@4.1.1", + "httpcore@4.1": "pkg:maven/org.apache.httpcomponents/httpcore@4.1", + "hudson-cli@": "pkg:maven/hudson-cli/hudson-cli", + "hudson-core@1.390": "pkg:maven/org.jvnet.hudson.main/hudson-core@1.390", + "hudson-war@1.390": "pkg:maven/org.jvnet.hudson.main/hudson-war@1.390", + "j-interop@2.0.5": "pkg:maven/j-interop/j-interop@2.0.5", + "j-interopdeps@2.0.5": "pkg:maven/j-interopdeps/j-interopdeps@2.0.5", + "jaxen@1.1-beta-11": "pkg:maven/org.jaxen/jaxen@1.1-beta-11", + "jcaptcha-all@1.0-RC6": "pkg:maven/jcaptcha-all/jcaptcha-all@1.0-RC6", + "jcifs@1.3.14-kohsuke-1": "pkg:maven/org.samba.jcifs/jcifs@1.3.14-kohsuke-1", + "jcommon@1.0.12": "pkg:maven/jfree/jcommon@1.0.12", + "jfreechart@1.0.9": "pkg:maven/jfreechart/jfreechart@1.0.9", + "jinterop-proxy@1.1": "pkg:maven/org.kohsuke.jinterop/jinterop-proxy@1.1", + "jinterop-wmi@1.0": "pkg:maven/org.jvnet.hudson/jinterop-wmi@1.0", + "jline@0.9.94": "pkg:maven/jline/jline@0.9.94", + "jmdns@3.1.6-hudson-2": "pkg:maven/com.strangeberry.jmdns.tools.Main/jmdns@3.1.6-hudson-2", + "jna-posix@1.0.3": "pkg:maven/org.jruby.ext.posix/jna-posix@1.0.3", + "jna@3.2.4": "pkg:maven/com.sun.jna/jna@3.2.4", + "jsch@0.1.27": "pkg:maven/jsch/jsch@0.1.27", + "json-lib@2.1-rev6": "pkg:maven/json-lib/json-lib@2.1-rev6", + "json@20200518": "pkg:maven/org.json/json@20200518", + "jstl@1.1.0": "pkg:maven/com.sun/jstl@1.1.0", + "jtidy@4aug2000r7-dev-hudson-1": "pkg:maven/jtidy/jtidy@4aug2000r7-dev-hudson-1", + "junit@4.13.1": "pkg:maven/junit/junit@4.13.1", + "kotlin-stdlib-common@1.3.70": "pkg:maven/kotlin-stdlib-common/kotlin-stdlib-common@1.3.70", + "kotlin-stdlib@1.3.70": "pkg:maven/kotlin-stdlib/kotlin-stdlib@1.3.70", + "libpam4j@1.2": "pkg:maven/org.jvnet.libpam4j/libpam4j@1.2", + "libzfs@0.5": "pkg:maven/org.jvnet.libzfs/libzfs@0.5", + "localizer@1.10": "pkg:maven/org.jvnet.localizer/localizer@1.10", + "log4j@1.2.9": "pkg:maven/log4j/log4j@1.2.9", + "logkit@1.0.1": "pkg:maven/logkit/logkit@1.0.1", + "mail@1.4": "pkg:maven/com.sun/mail@1.4", + "maven-agent@1.390": "pkg:maven/org.jvnet.hudson.main/maven-agent@1.390", + "maven-artifact-manager@2.0.9": "pkg:maven/org.apache.maven/maven-artifact-manager@2.0.9", + "maven-artifact@2.0.9": "pkg:maven/org.apache.maven/maven-artifact@2.0.9", + "maven-core@2.0.9": "pkg:maven/org.apache.maven/maven-core@2.0.9", + "maven-embedder@2.0.4": "pkg:maven/org.apache.maven/maven-embedder@2.0.4", + "maven-embedder@2.0.4-hudson-1": "pkg:maven/org.jvnet.hudson/maven-embedder@2.0.4-hudson-1", + "maven-error-diagnostics@2.0.9": "pkg:maven/org.apache.maven/maven-error-diagnostics@2.0.9", + "maven-interceptor@1.390": "pkg:maven/org.jvnet.hudson.main/maven-interceptor@1.390", + "maven-model@2.0.9": "pkg:maven/org.apache.maven/maven-model@2.0.9", + "maven-monitor@2.0.9": "pkg:maven/org.apache.maven/maven-monitor@2.0.9", + "maven-plugin-api@2.0.9": "pkg:maven/org.apache.maven/maven-plugin-api@2.0.9", + "maven-plugin-descriptor@2.0.9": "pkg:maven/org.apache.maven/maven-plugin-descriptor@2.0.9", + "maven-plugin-parameter-documenter@2.0.9": "pkg:maven/org.apache.maven/maven-plugin-parameter-documenter@2.0.9", + "maven-plugin-registry@2.0.9": "pkg:maven/org.apache.maven/maven-plugin-registry@2.0.9", + "maven-plugin@1.390": "pkg:maven/org.jvnet.hudson.main/maven-plugin@1.390", + "maven-profile@2.0.9": "pkg:maven/org.apache.maven/maven-profile@2.0.9", + "maven-project@2.0.9": "pkg:maven/org.apache.maven/maven-project@2.0.9", + "maven-reporting-api@2.0.9": "pkg:maven/org.apache.maven.reporting/maven-reporting-api@2.0.9", + "maven-repository-metadata@2.0.9": "pkg:maven/org.apache.maven/maven-repository-metadata@2.0.9", + "maven-settings@2.0.9": "pkg:maven/org.apache.maven/maven-settings@2.0.9", + "maven2.1-interceptor@1.2": "pkg:maven/org.jvnet.hudson/maven2.1-interceptor@1.2", + "memory-monitor@1.3": "pkg:maven/org.jvnet.hudson/memory-monitor@1.3", + "nomad@0.7.4": "pkg:maven/org.jenkins-ci.plugins/nomad@0.7.4", + "okhttp@4.5.0": "pkg:maven/com.squareup.okhttp3/okhttp@4.5.0", + "okio@2.5.0": "pkg:maven/com.squareup.okio/okio@2.5.0", + "oro@2.0.8": "pkg:maven/org.apache.oro/oro@2.0.8", + "plexus-container-default@1.0-alpha-9-stable-1": "pkg:maven/org.codehaus.plexus/plexus-container-default@1.0-alpha-9-stable-1", + "plexus-interactivity-api@1.0-alpha-4": "pkg:maven/org.codehaus.plexus/plexus-interactivity-api@1.0-alpha-4", + "plexus-utils@1.5.1": "pkg:maven/org.codehaus.plexus/plexus-utils@1.5.1", + "remoting@1.390": "pkg:maven/org.jvnet.hudson.main/remoting@1.390", + "robust-http-client@1.1": "pkg:maven/org.jvnet.robust-http-client/robust-http-client@1.1", + "sdk@3.0": "pkg:maven/sdk/sdk@3.0", + "sezpoz@1.7": "pkg:maven/net.java.sezpoz/sezpoz@1.7", + "slave@": "pkg:maven/slave/slave", + "slide-webdavlib@2.1": "pkg:maven/slide-webdavlib/slide-webdavlib@2.1", + "spring-aop@2.5": "pkg:maven/org.springframework.bundle.spring.aop/spring-aop@2.5", + "spring-beans@2.5": "pkg:maven/org.springframework/spring-beans@2.5", + "spring-context@2.5": "pkg:maven/org.springframework.bundle.spring.context/spring-context@2.5", + "spring-core@2.5": "pkg:maven/org.springframework/spring-core@2.5", + "spring-dao@1.2.9": "pkg:maven/spring-dao/spring-dao@1.2.9", + "spring-jdbc@1.2.9": "pkg:maven/spring-jdbc/spring-jdbc@1.2.9", + "spring-web@2.5": "pkg:maven/org.springframework/spring-web@2.5", + "ssh-slaves@0.14": "pkg:maven/org.jvnet.hudson.plugins/ssh-slaves@0.14", + "stapler-adjunct-timeline@1.2": "pkg:maven/org.kohsuke.stapler/stapler-adjunct-timeline@1.2", + "stapler-jelly@1.155": "pkg:maven/org.kohsuke.stapler/stapler-jelly@1.155", + "stapler@1.155": "pkg:maven/org.kohsuke.stapler/stapler@1.155", + "stax-api@1.0.1": "pkg:maven/stax-api/stax-api@1.0.1", + "subversion@1.20": "pkg:maven/org.jvnet.hudson.plugins/subversion@1.20", + "svnkit@1.3.4-hudson-2": "pkg:maven/svnkit/svnkit@1.3.4-hudson-2", + "task-reactor@1.2": "pkg:maven/org.jvnet.hudson/task-reactor@1.2", + "tiger-types@1.3": "pkg:maven/org.jvnet/tiger-types@1.3", + "trilead-putty-extension@1.0": "pkg:maven/org.kohsuke/trilead-putty-extension@1.0", + "trilead-ssh2@build212-hudson-5": "pkg:maven/org.jvnet.hudson/trilead-ssh2@build212-hudson-5", + "txw2@20070624": "pkg:maven/txw2/txw2@20070624", + "wagon-file@1.0-beta-2": "pkg:maven/org.apache.maven.wagon/wagon-file@1.0-beta-2", + "wagon-http-lightweight@1.0-beta-2": "pkg:maven/org.apache.maven.wagon/wagon-http-lightweight@1.0-beta-2", + "wagon-http-shared@1.0-beta-2": "pkg:maven/org.apache.maven.wagon/wagon-http-shared@1.0-beta-2", + "wagon-provider-api@1.0-beta-2": "pkg:maven/org.apache.maven.wagon/wagon-provider-api@1.0-beta-2", + "wagon-ssh-common@1.0-beta-2": "pkg:maven/org.apache.maven.wagon/wagon-ssh-common@1.0-beta-2", + "wagon-ssh-external@1.0-beta-2": "pkg:maven/org.apache.maven.wagon/wagon-ssh-external@1.0-beta-2", + "wagon-ssh@1.0-beta-2": "pkg:maven/org.apache.maven.wagon/wagon-ssh@1.0-beta-2", + "wagon-webdav@1.0-beta-2-hudson-1": "pkg:maven/org.jvnet.hudson/wagon-webdav@1.0-beta-2-hudson-1", + "windows-remote-command@1.0": "pkg:maven/org.jvnet.hudson/windows-remote-command@1.0", + "winp@1.14": "pkg:maven/org.jvnet.winp/winp@1.14", + "winstone@0.9.10-hudson-24": "pkg:maven/org.jvnet.hudson.winstone/winstone@0.9.10-hudson-24", + "wstx-asl@3.2.7": "pkg:maven/wstx-asl/wstx-asl@3.2.7", + "xml-im-exporter@1.1": "pkg:maven/xml-im-exporter/xml-im-exporter@1.1", + "xpp3@1.1.4c": "pkg:maven/xpp3/xpp3@1.1.4c", + "xpp3_min@1.1.4c": "pkg:maven/xpp3_min/xpp3_min@1.1.4c", + "xstream@1.3.1-hudson-8": "pkg:maven/org.jvnet.hudson/xstream@1.3.1-hudson-8", +} diff --git a/test/integration/regression_go_bin_scanner_arch_test.go b/test/integration/regression_go_bin_scanner_arch_test.go index 8a51a9a77..d88ee6c7c 100644 --- a/test/integration/regression_go_bin_scanner_arch_test.go +++ b/test/integration/regression_go_bin_scanner_arch_test.go @@ -10,9 +10,9 @@ import ( func TestRegressionGoArchDiscovery(t *testing.T) { const ( - expectedELFPkg = 4 - expectedWINPkg = 4 - expectedMACOSPkg = 4 + expectedELFPkg = 5 + expectedWINPkg = 5 + expectedMACOSPkg = 5 ) // This is a regression test to make sure the way we detect go binary packages // stays consistent and reproducible as the tool chain evolves diff --git a/test/integration/regression_java_virtualpath_test.go b/test/integration/regression_java_virtualpath_test.go new file mode 100644 index 000000000..659e5d017 --- /dev/null +++ b/test/integration/regression_java_virtualpath_test.go @@ -0,0 +1,36 @@ +package integration + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func TestWarCatalogedCorrectlyIfRenamed(t *testing.T) { + // install hudson-war@2.2.1 and renames the file to `/hudson.war` + sbom, _ := catalogFixtureImage(t, "image-java-virtualpath-regression", source.SquashedScope, nil) + + badPURL := "pkg:maven/hudson/hudson@2.2.1" + goodPURL := "pkg:maven/org.jvnet.hudson.main/hudson-war@2.2.1" + foundCorrectPackage := false + badVirtualPath := "/hudson.war:org.jvnet.hudson.main:hudson-war" + goodVirtualPath := "/hudson.war" + for _, p := range sbom.Artifacts.Packages.Sorted() { + if p.Type == pkg.JavaPkg && strings.Contains(p.Name, "hudson") { + assert.NotEqual(t, badPURL, p.PURL, "must not find bad purl %q", badPURL) + virtPath := "" + if meta, ok := p.Metadata.(pkg.JavaMetadata); ok { + virtPath = meta.VirtualPath + if p.PURL == goodPURL && virtPath == goodVirtualPath { + foundCorrectPackage = true + } + } + assert.NotEqual(t, badVirtualPath, virtPath, "must not find bad virtual path %q", badVirtualPath) + } + } + assert.True(t, foundCorrectPackage, "must find correct package, but did not") +} diff --git a/test/integration/test-fixtures/image-distro-id/Dockerfile b/test/integration/test-fixtures/image-distro-id/Dockerfile index 400d03032..0983819b3 100644 --- a/test/integration/test-fixtures/image-distro-id/Dockerfile +++ b/test/integration/test-fixtures/image-distro-id/Dockerfile @@ -1,3 +1,3 @@ -FROM busybox:1.31.1 +FROM busybox:1.31.1@sha256:95cf004f559831017cdf4628aaf1bb30133677be8702a8c5f2994629f637a209 diff --git a/test/integration/test-fixtures/image-go-bin-arch-coverage/Dockerfile b/test/integration/test-fixtures/image-go-bin-arch-coverage/Dockerfile index 7b252568c..6bf28daaa 100644 --- a/test/integration/test-fixtures/image-go-bin-arch-coverage/Dockerfile +++ b/test/integration/test-fixtures/image-go-bin-arch-coverage/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:latest as builder +FROM golang:1.21.1@sha256:cffaba795c36f07e372c7191b35ceaae114d74c31c3763d442982e3a4df3b39e as builder WORKDIR /app COPY go.sum go.mod app.go ./ diff --git a/test/integration/test-fixtures/image-golang-compiler/Dockerfile b/test/integration/test-fixtures/image-golang-compiler/Dockerfile new file mode 100644 index 000000000..2d8e6bbdc --- /dev/null +++ b/test/integration/test-fixtures/image-golang-compiler/Dockerfile @@ -0,0 +1 @@ +FROM golang:1.18.10-alpine \ No newline at end of file diff --git a/test/integration/test-fixtures/image-java-no-main-package/Dockerfile b/test/integration/test-fixtures/image-java-no-main-package/Dockerfile index 7e1ac4de7..dce8deba3 100644 --- a/test/integration/test-fixtures/image-java-no-main-package/Dockerfile +++ b/test/integration/test-fixtures/image-java-no-main-package/Dockerfile @@ -1,4 +1,4 @@ -FROM jenkins/jenkins:2.346.3-slim-jdk17 +FROM jenkins/jenkins:2.346.3-slim-jdk17@sha256:028fbbd9112c60ed086f5197fcba71992317864d27644e5949cf9c52ff4b65f0 USER root diff --git a/test/integration/test-fixtures/image-java-virtualpath-regression/Dockerfile b/test/integration/test-fixtures/image-java-virtualpath-regression/Dockerfile new file mode 100644 index 000000000..63fc6c92a --- /dev/null +++ b/test/integration/test-fixtures/image-java-virtualpath-regression/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine:3.18.3@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a + +RUN wget https://repo1.maven.org/maven2/org/jvnet/hudson/main/hudson-war/2.2.1/hudson-war-2.2.1.war + +RUN mv hudson-war-2.2.1.war hudson.war + + diff --git a/test/integration/test-fixtures/image-os-binary-overlap/Dockerfile b/test/integration/test-fixtures/image-os-binary-overlap/Dockerfile index 0951649a3..21c4d8479 100644 --- a/test/integration/test-fixtures/image-os-binary-overlap/Dockerfile +++ b/test/integration/test-fixtures/image-os-binary-overlap/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:latest +FROM alpine:3.18.3@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a # syft should not longer show the binary package for this image: # https://github.com/anchore/syft/issues/931 diff --git a/test/integration/test-fixtures/image-owning-package/Dockerfile b/test/integration/test-fixtures/image-owning-package/Dockerfile index 37346c9c7..192998626 100644 --- a/test/integration/test-fixtures/image-owning-package/Dockerfile +++ b/test/integration/test-fixtures/image-owning-package/Dockerfile @@ -1,3 +1,3 @@ -FROM ubuntu:20.04 +FROM ubuntu:20.04@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba # this covers rpm-python -RUN apt-get update && apt-get install -y python-pil=6.2.1-3 \ No newline at end of file +RUN apt-get update && apt-get install -y python-pil=6.2.1-3 diff --git a/test/integration/test-fixtures/image-photon-all-layers/Dockerfile b/test/integration/test-fixtures/image-photon-all-layers/Dockerfile index ab09f97ad..17bb3691b 100644 --- a/test/integration/test-fixtures/image-photon-all-layers/Dockerfile +++ b/test/integration/test-fixtures/image-photon-all-layers/Dockerfile @@ -1 +1 @@ -FROM photon:5.0-20230729 +FROM photon:5.0-20230729@sha256:4cf2a1ce0a3f4625f13a0becb6b9bccfdb014c565be6e9a2ec4c4aad1ff8a5d9 diff --git a/test/integration/test-fixtures/image-pkg-coverage/pkgs/github-actions/.github/workflows/validations.yaml b/test/integration/test-fixtures/image-pkg-coverage/pkgs/github-actions/.github/workflows/validations.yaml new file mode 100644 index 000000000..2c8c17526 --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/pkgs/github-actions/.github/workflows/validations.yaml @@ -0,0 +1,18 @@ +name: "Validations" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + call-workflow-1-in-local-repo: + uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89 + + Unit-Test: + name: "Unit tests" + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 diff --git a/test/integration/test-fixtures/image-rust-auditable/Dockerfile b/test/integration/test-fixtures/image-rust-auditable/Dockerfile index a35897003..64540e99f 100644 --- a/test/integration/test-fixtures/image-rust-auditable/Dockerfile +++ b/test/integration/test-fixtures/image-rust-auditable/Dockerfile @@ -1,2 +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 +FROM docker.io/tofay/hello-rust-auditable@sha256:1d35d1e007180b3f7500aae5e27560697909132ca9a6d480c4c825534c1c47a9 diff --git a/test/integration/test-fixtures/image-suse-all-layers/Dockerfile b/test/integration/test-fixtures/image-suse-all-layers/Dockerfile index c8d708b59..339983d88 100644 --- a/test/integration/test-fixtures/image-suse-all-layers/Dockerfile +++ b/test/integration/test-fixtures/image-suse-all-layers/Dockerfile @@ -1,2 +1,2 @@ -FROM registry.suse.com/suse/sle15:15.3.17.20.20 +FROM registry.suse.com/suse/sle15:15.3.17.20.20@sha256:fd657ecbab5ca564d6933e887f6ae8542a9398e6a4b399f352ce10c3a24afc64 RUN zypper in -y wget diff --git a/test/integration/test-fixtures/image-test-java-purls/Dockerfile b/test/integration/test-fixtures/image-test-java-purls/Dockerfile new file mode 100644 index 000000000..05545a7ce --- /dev/null +++ b/test/integration/test-fixtures/image-test-java-purls/Dockerfile @@ -0,0 +1 @@ +FROM anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da diff --git a/test/integration/test-fixtures/image-vertical-package-dups/Dockerfile b/test/integration/test-fixtures/image-vertical-package-dups/Dockerfile index 8f4f2a3a6..cd0e69b5d 100644 --- a/test/integration/test-fixtures/image-vertical-package-dups/Dockerfile +++ b/test/integration/test-fixtures/image-vertical-package-dups/Dockerfile @@ -1,6 +1,6 @@ -FROM centos:7.9.2009 +FROM centos:7.9.2009@sha256:be65f488b7764ad3638f236b7b515b3678369a5124c47b8d32916d6487418ea4 # modifying the RPM DB multiple times will result in duplicate packages when using all-layers (if there was no de-dup logic) # curl is tricky, it already exists in the image and is being upgraded RUN yum install -y wget-1.14-18.el7_6.1 curl-7.29.0-59.el7_9.1 RUN yum install -y vsftpd-3.0.2-29.el7_9 -RUN yum install -y httpd-2.4.6-97.el7.centos.5 \ No newline at end of file +RUN yum install -y httpd-2.4.6-97.el7.centos.5 diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go index 207fe675c..eeb583f65 100644 --- a/test/integration/utils_test.go +++ b/test/integration/utils_test.go @@ -8,8 +8,6 @@ import ( "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/pkg/cataloger" - "github.com/anchore/syft/syft/pkg/cataloger/kernel" - "github.com/anchore/syft/syft/pkg/cataloger/python" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -26,7 +24,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Sco theSource.Close() }) - c := defaultConfig() + c := cataloger.DefaultConfig() c.Catalogers = catalogerCfg c.Search.Scope = scope @@ -54,16 +52,6 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Sco }, theSource } -func defaultConfig() cataloger.Config { - return cataloger.Config{ - Search: cataloger.DefaultSearchConfig(), - Parallelism: 1, - LinuxKernel: kernel.DefaultLinuxCatalogerConfig(), - Python: python.DefaultCatalogerConfig(), - ExcludeBinaryOverlapByOwnership: true, - } -} - func catalogDirectory(t *testing.T, dir string) (sbom.SBOM, source.Source) { userInput := "dir:" + dir detection, err := source.Detect(userInput, source.DefaultDetectConfig()) @@ -75,7 +63,7 @@ func catalogDirectory(t *testing.T, dir string) (sbom.SBOM, source.Source) { }) // TODO: this would be better with functional options (after/during API refactor) - c := defaultConfig() + c := cataloger.DefaultConfig() c.Search.Scope = source.AllLayersScope pkgCatalog, relationships, actualDistro, err := syft.CatalogPackages(theSource, c) if err != nil {